diff --git a/app/users/[id]/page.module.css b/app/users/[id]/page.module.css
new file mode 100644
index 0000000..64b7bd0
--- /dev/null
+++ b/app/users/[id]/page.module.css
@@ -0,0 +1,114 @@
+.main {
+ gap: var(--main-padding);
+}
+
+.notices {
+ display: flex;
+ flex-flow: column;
+ gap: 0.8rem;
+}
+
+.mainGraphContainer {
+ display: flex;
+ flex-flow: column;
+ gap: var(--internal-gap);
+}
+
+.graphContainer {
+ display: flex;
+ flex-flow: column;
+ background-color: hsla(var(--gray-100));
+ border-radius: var(--main-borderRadius);
+ padding: var(--main-padding);
+ gap: 1.2rem;
+}
+
+.graphContainer .header {
+ width: 100%;
+ display: flex;
+ flex-flow: row;
+ align-items: flex-end;
+ justify-content: space-between;
+}
+
+.graphContainer .chart,
+.chart {
+ height: 18rem;
+}
+
+.header .rating {
+ display: flex;
+ flex-flow: row;
+ font-size: 3.4rem;
+ font-weight: 600;
+ gap: 0.6rem;
+ line-height: 3.4rem;
+}
+
+.rating .change {
+ font-size: 1.6rem;
+ line-height: 2.2rem;
+ display: flex;
+ flex-flow: row;
+ align-items: flex-end;
+}
+
+.change.positive {
+ color: hsla(var(--green-400));
+}
+
+.change.positive::before {
+ content: '+';
+}
+
+.change.negative {
+ color: hsla(var(--red-400));
+}
+
+.change.negative::before {
+ content: '-';
+}
+
+.header .stats {
+ display: flex;
+ flex-flow: row;
+ gap: 4rem;
+ align-items: flex-end;
+}
+
+.stats .item {
+ display: flex;
+ flex-flow: row;
+ gap: 1.1rem;
+ font-size: 1.1rem;
+ font-weight: 400;
+}
+
+.item .score {
+ font-weight: 600;
+}
+
+.item #rank::before {
+ content: '#';
+}
+
+.item #percentile::after {
+ content: '%';
+}
+
+.cardStat {
+ display: flex;
+ flex-flow: row;
+ gap: 1rem;
+ font-size: 1.2rem;
+}
+
+.cardStat .value {
+ font-weight: 600;
+}
+
+.noGraphText {
+ font-style: italic;
+ font-weight: 400;
+ font-size: 0.9rem;
+}
diff --git a/app/users/[id]/page.tsx b/app/users/[id]/page.tsx
new file mode 100644
index 0000000..d0993d0
--- /dev/null
+++ b/app/users/[id]/page.tsx
@@ -0,0 +1,211 @@
+import { fetchUserPage, fetchUserPageTitle } from '@/app/actions';
+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';
+import StatsGrid from '@/components/Dashboard/StatsGrid/StatsGrid';
+import FormattedNumber from '@/components/FormattedNumber/FormattedNumber';
+import UserMainCard from '@/components/Profile/UserMainCard/UserMainCard';
+import clsx from 'clsx';
+import styles from './page.module.css';
+
+export async function generateMetadata({
+ params: { id },
+}: {
+ params: { id: string | number };
+}) {
+ let player = await fetchUserPageTitle(id);
+
+ return {
+ title: player !== null ? `${player?.username}'s profile` : 'User profile',
+ };
+}
+
+export const revalidate = 60;
+
+export default async function page({
+ searchParams,
+ params: { id },
+}: {
+ searchParams: string | string[] | {};
+ params: { id: string | number };
+}) {
+ const data = await fetchUserPage(id);
+
+ return (
+
+
+
+
+
+
+
+ {Math.round(data.generalStats.rating)}
+ = 0
+ ? styles.positive
+ : styles.negative
+ )}
+ >
+ {data.matchStats.ratingGained.toFixed(0) !== 0 &&
+ data.matchStats.ratingGained.toFixed(0)}
+
+
+
+
+
Highest rating
+
+ {data.matchStats.highestRating.toFixed(0)}
+
+
+
+
+
Highest percentile
+
+ {(data.matchStats.highestPercentile * 100).toFixed(1)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Average opponent rating
+
+ {data?.matchStats.averageOpponentRating.toFixed(0)}
+
+
+
+ Average teammate rating
+
+ {data?.matchStats.averageTeammateRating.toFixed(0)}
+
+
+
+ Best win streak
+
+ {data?.matchStats.bestWinStreak}
+
+
+
+
+
+ Average score
+
+
+
+
+
+ Average misses
+
+ {data?.matchStats.matchAverageMissesAggregate.toFixed(0)}
+
+
+
+ Average accuracy
+
+ {data?.matchStats.matchAverageAccuracyAggregate.toFixed(2)}%
+
+
+
+ Average maps played
+
+ {data?.matchStats.averageGamesPlayedAggregate.toFixed(0)}
+
+
+
+
+
+
+
+
+ Most played
+
+ {data?.matchStats.mostPlayedTeammateName}
+
+
+
+ Best teammate
+
+ {data?.matchStats.bestTeammateName}
+
+
+
+
+
+ Most played
+
+ {data?.matchStats.mostPlayedOpponentName}
+
+
+
+ Best opponent
+ TO DO
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {data?.tournamentStats.worstPerformances.length > 0 ? (
+
+ ) : (
+
+ Play 6 or more tournaments to populate this graph!
+
+ )}
+
+
+
+ );
+}
diff --git a/components/Badges/Provisional/ProvisionalBadge.tsx b/components/Badges/Provisional/ProvisionalBadge.tsx
new file mode 100644
index 0000000..40fe984
--- /dev/null
+++ b/components/Badges/Provisional/ProvisionalBadge.tsx
@@ -0,0 +1,11 @@
+import provisionalImage from '@/public/icons/provisional.png';
+import Image from 'next/image';
+import styles from './provisionalBadge.module.css';
+
+export default function ProvisionalBadge() {
+ return (
+
+
+
+ );
+}
diff --git a/components/Badges/Provisional/provisionalBadge.module.css b/components/Badges/Provisional/provisionalBadge.module.css
new file mode 100644
index 0000000..4fe4d13
--- /dev/null
+++ b/components/Badges/Provisional/provisionalBadge.module.css
@@ -0,0 +1,45 @@
+.badge {
+ width: 1.8em;
+ height: 1.8em;
+ aspect-ratio: 1;
+ font-size: 0.26em;
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ justify-content: center;
+ color: hsl(var(--badge-provisional-foreground));
+ user-select: none;
+ position: relative;
+ cursor: pointer;
+}
+
+.badge img {
+ object-fit: cover;
+}
+
+.badge:after {
+ content: 'Rating is provisional, must play more matches for an accurate ranking';
+ width: max-content;
+ max-width: 500px;
+ display: flex;
+ flex-flow: row;
+ background-color: hsla(var(--badge-provisional-background), 0.3);
+ line-height: 1.2rem;
+ backdrop-filter: blur(10px);
+ position: absolute;
+ left: 2.2rem;
+ bottom: -0.3rem;
+ font-size: 0.8rem;
+ padding: 0.6rem 0.8rem;
+ border-radius: 4px;
+ visibility: hidden;
+ opacity: 0;
+ transition: opacity 0.2s ease-out, visibility 0.2s ease-out;
+ z-index: 2;
+ font-weight: 600;
+}
+
+.badge:hover::after {
+ visibility: visible;
+ opacity: 1;
+}
diff --git a/components/Button/LoginButton.tsx b/components/Button/LoginButton.tsx
index ee1a4a4..7983795 100644
--- a/components/Button/LoginButton.tsx
+++ b/components/Button/LoginButton.tsx
@@ -1,26 +1,30 @@
'use client';
import { loginIntoWebsite } from '@/app/actions';
+import { useUser } from '@/util/hooks';
export default function LoginButton() {
- return (
-
- );
+ const user = useUser();
+
+ if (!user?.osuId)
+ return (
+
+ );
}
diff --git a/components/Charts/AreaChart/AreaChart.module.css b/components/Charts/AreaChart/AreaChart.module.css
index 4323e9c..930b785 100644
--- a/components/Charts/AreaChart/AreaChart.module.css
+++ b/components/Charts/AreaChart/AreaChart.module.css
@@ -6,7 +6,7 @@
}
.tooltip {
- background: rgba(0, 0, 0, 0.4);
+ background: rgba(0, 0, 0, 0.7);
border-radius: 0.2rem;
color: #fff;
opacity: 0;
@@ -14,4 +14,64 @@
position: absolute;
transform: translate(0%, 0);
transition: all 0.1s ease;
+ backdrop-filter: blur(2px);
+ min-width: 10em;
+}
+
+.tooltip .header {
+ width: 100%;
+ display: flex;
+ flex-flow: row;
+ font-weight: 600;
+ gap: 0 0.3rem;
+ padding: 0.2rem;
+ color: hsla(var(--tooltip-color), 0.92);
+ cursor: default;
+ pointer-events: auto;
+}
+
+.tooltip ul {
+ gap: 1rem;
+ pointer-events: auto;
+}
+
+.tooltip li {
+ display: flex;
+ flex-flow: row;
+ gap: 0 0.8rem;
+ font-size: 0.9rem;
+ align-items: center;
+ justify-content: space-between;
+ font-weight: 500;
+ color: hsla(var(--tooltip-color), 0.8);
+ padding: 0.2rem;
+}
+
+.tooltip li a:hover {
+ color: hsla(var(--tooltip-color));
+}
+
+.tooltip_ratingChange {
+ font-weight: 600;
+ color: #eee;
+ display: flex;
+ flex-flow: row;
+ gap: 0 0.14rem;
+ cursor: default;
+}
+
+.tooltip_ratingChange.gain {
+ color: hsl(var(--tooltip-gain));
+}
+
+.tooltip_ratingChange.gain::before {
+ content: '▲';
+}
+
+.tooltip_ratingChange.loss {
+ color: hsl(var(--tooltip-loss));
+}
+
+.tooltip_ratingChange.loss::before {
+ content: '▼';
}
diff --git a/components/Charts/AreaChart/AreaChart.tsx b/components/Charts/AreaChart/AreaChart.tsx
index 5af7864..0dd3f6c 100644
--- a/components/Charts/AreaChart/AreaChart.tsx
+++ b/components/Charts/AreaChart/AreaChart.tsx
@@ -11,6 +11,7 @@ import {
Title,
Tooltip,
} from 'chart.js';
+import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { Line } from 'react-chartjs-2';
import styles from './AreaChart.module.css';
@@ -26,7 +27,6 @@ ChartJS.register(
Legend
);
-/*
const getOrCreateTooltip = (chart) => {
let tooltipEl = chart.canvas.parentNode.querySelector('div');
@@ -34,101 +34,17 @@ const getOrCreateTooltip = (chart) => {
tooltipEl = document.createElement('div');
tooltipEl.className = styles.tooltip;
- const table = document.createElement('table');
- table.style.margin = '0px';
+ const div = document.createElement('div');
+ div.className = 'tooltip';
+ div.style.margin = '0px';
- tooltipEl.appendChild(table);
+ tooltipEl.appendChild(div);
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
};
-export const externalTooltipHandler = (context) => {
- // Tooltip Element
- const { chart, tooltip } = context;
- const tooltipEl = getOrCreateTooltip(chart);
-
-
-
- // Hide if no tooltip
- if (tooltip.opacity === 0) {
- tooltipEl.style.opacity = 0;
- return;
- }
-
- // Set Text
- if (tooltip.body) {
- const titleLines = tooltip.title || [];
- const bodyLines = tooltip.body.map((b) => b.lines);
-
- const tableHead = document.createElement('thead');
-
- titleLines.forEach((title) => {
- const tr = document.createElement('tr');
- tr.style.borderWidth = 0;
-
- const th = document.createElement('th');
- th.style.borderWidth = 0;
- const text = document.createTextNode(title);
-
- th.appendChild(text);
- tr.appendChild(th);
- tableHead.appendChild(tr);
- });
-
- const tableBody = document.createElement('tbody');
- bodyLines.forEach((body, i) => {
- const colors = tooltip.labelColors[i];
-
- const span = document.createElement('span');
- span.style.background = colors.backgroundColor;
- span.style.borderColor = colors.borderColor;
- span.style.borderWidth = '2px';
- span.style.marginRight = '10px';
- span.style.height = '10px';
- span.style.width = '10px';
- span.style.display = 'inline-block';
-
- const tr = document.createElement('tr');
- tr.style.backgroundColor = 'inherit';
- tr.style.borderWidth = 0;
-
- const td = document.createElement('td');
- td.style.borderWidth = 0;
-
- const text = document.createTextNode(body);
-
- td.appendChild(span);
- td.appendChild(text);
- tr.appendChild(td);
- tableBody.appendChild(tr);
- });
-
- const tableRoot = tooltipEl.querySelector('table');
-
- // Remove old children
- while (tableRoot.firstChild) {
- tableRoot.firstChild.remove();
- }
-
- // Add new children
- tableRoot.appendChild(tableHead);
- tableRoot.appendChild(tableBody);
- }
-
- const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas;
-
- // Display, position, and set styles for font
- tooltipEl.style.opacity = 1;
- tooltipEl.style.left = positionX + tooltip.caretX + 'px';
- tooltipEl.style.top = positionY + tooltip.caretY + 'px';
- tooltipEl.style.font = tooltip.options.bodyFont.string;
- tooltipEl.style.padding =
- tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
-};
- */
-
export default function AreaChart({
ratingStats,
rankChart,
@@ -143,8 +59,9 @@ export default function AreaChart({
/* get variables of colors from CSS */
useEffect(() => {
setColors([
- getComputedStyle(document.documentElement).getPropertyValue('--blue-600'),
- getComputedStyle(document.documentElement).getPropertyValue('--blue-400'),
+ getComputedStyle(document.documentElement).getPropertyValue(
+ '--accent-color'
+ ),
]);
setFont(
getComputedStyle(document.documentElement).getPropertyValue(
@@ -153,22 +70,163 @@ export default function AreaChart({
);
}, []);
+ const dateFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' };
+
+ let labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
+ let dataForGraph: number[] = labels.map(() => Math.ceil(Math.random() * 100));
+ let tournamentsTooltip: object[];
+
+ if (ratingStats) {
+ labels = ratingStats.map((day) => {
+ return new Date(day[0].tooltipInfo.matchDate).toLocaleDateString(
+ 'en-US',
+ dateFormatOptions
+ );
+ });
+
+ tournamentsTooltip = ratingStats.map((day) => {
+ let matches = [];
+ day.forEach((match) => {
+ match.tooltipInfo.matchDate = new Date(
+ match.tooltipInfo.matchDate
+ ).toLocaleDateString('en-US', dateFormatOptions);
+
+ return matches.push(match);
+ });
+ return matches;
+ });
+
+ dataForGraph = ratingStats.map((day) => {
+ if (day.length > 1) {
+ return day[day.length - 1].ratingAfter.toFixed(0);
+ }
+
+ if (day.length === 1) {
+ return day[0].ratingAfter.toFixed(0);
+ }
+ });
+ }
+
+ /* if (rankChart) {
+ labels = rankChart.map((match) => match.matchName);
+ dataForGraph = rankChart.map((match) => match.rank.toFixed(0));
+ } */
+
+ const data = {
+ labels,
+ datasets: [
+ {
+ fill: true,
+ label: '',
+ data: dataForGraph,
+ borderWidth: 3,
+ borderColor: `hsla(${colors[0]}, 0.6)`,
+ backgroundColor: 'transparent',
+ font: font,
+ },
+ ],
+ };
+
+ const externalTooltipHandler = (context) => {
+ // Tooltip Element
+ const { chart, tooltip } = context;
+ const tooltipEl = getOrCreateTooltip(chart);
+
+ // Hide if no tooltip
+ if (tooltip.opacity === 0) {
+ tooltipEl.style.opacity = 0;
+ return;
+ }
+
+ if (tooltip.body && ratingStats) {
+ const matchesLines = new Set(
+ ...tournamentsTooltip.filter(
+ (day) => day[0].tooltipInfo.matchDate == tooltip.title
+ )
+ );
+
+ /* TOOLTIP HEADER */
+ const header = document.createElement('div');
+ header.className = styles.header;
+ const headerProperty = document.createElement('span');
+ headerProperty.className = styles.headerProperty;
+ headerProperty.innerHTML = 'Rating:';
+ const headerValue = document.createElement('span');
+ headerValue.className = styles.headerValue;
+ headerValue.innerHTML = tooltip.body[0].lines[0];
+
+ header.appendChild(headerProperty);
+ header.appendChild(headerValue);
+
+ const matchesList = document.createElement('ul');
+ matchesLines.forEach((match) => {
+ const li = document.createElement('li');
+
+ const matchName = document.createElement('a');
+ matchName.href = match.tooltipInfo.mpLink;
+ matchName.target = '_blank';
+ matchName.innerHTML = match.tooltipInfo.matchName;
+
+ const ratingChange = document.createElement('span');
+ ratingChange.innerHTML = match.ratingChange.toFixed(1);
+ ratingChange.className = clsx(
+ styles.tooltip_ratingChange,
+ match.ratingChange.toFixed(1) > 0
+ ? styles.gain
+ : match.ratingChange.toFixed(1) < 0
+ ? styles.loss
+ : ''
+ );
+
+ li.appendChild(matchName);
+ li.appendChild(ratingChange);
+ matchesList.appendChild(li);
+ });
+
+ const divRoot = tooltipEl.querySelector('div.tooltip');
+
+ // Remove old children
+ while (divRoot.firstChild) {
+ divRoot.firstChild.remove();
+ }
+
+ divRoot.appendChild(header);
+ divRoot.appendChild(matchesList);
+ }
+
+ const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas;
+
+ // Display, position, and set styles for font
+ tooltipEl.style.opacity = 1;
+ tooltipEl.style.top = positionY + tooltip.caretY + 5 + 'px';
+ tooltipEl.style.font = tooltip.options.bodyFont.string;
+ tooltipEl.style.padding =
+ tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
+ tooltipEl.style.pointerEvents = 'none';
+
+ var offset = tooltip.width + 120;
+ if (chart.width / 2 < tooltip.caretX) {
+ offset *= -1;
+ } else {
+ offset *= -0.2;
+ }
+
+ // Hidden Code
+ tooltipEl.style.left = positionX + tooltip.caretX + offset + 'px';
+ };
+
const options = {
responsive: true,
+ events: ['mousemove', 'click', 'touchstart', 'touchmove'],
plugins: {
legend: {
display: false,
- /* position: 'top' as const, */
},
tooltip: {
enabled: false,
position: 'nearest',
- /* external: externalTooltipHandler, */
+ external: ratingStats ? externalTooltipHandler : null,
},
- /* title: {
- display: true,
- text: 'Chart.js Line Chart',
- }, */
},
elements: {
line: {
@@ -176,7 +234,8 @@ export default function AreaChart({
},
point: {
radius: 0 /* 0 makes points hidden */,
- hitRadius: 0,
+ hitRadius: 100,
+ pointBackgroundColor: `hsla(${colors[0]}, 0.6)`,
},
},
maintainAspectRatio: false,
@@ -187,10 +246,9 @@ export default function AreaChart({
size: 16,
family: font,
},
+ autoSkip: true,
+ maxTicksLimit: 8,
},
- /* border: {
- color: 'transparent',
- }, */
},
y: {
ticks: {
@@ -198,46 +256,15 @@ export default function AreaChart({
size: 16,
family: font,
},
+ autoSkip: true,
+ maxTicksLimit: 6,
+ precision: 0,
+ stepSize: 15,
},
},
},
};
- const dateFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' };
-
- let labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
- let dataForGraph: number[] = labels.map(() => Math.ceil(Math.random() * 100));
-
- if (ratingStats) {
- labels = ratingStats.map((match) =>
- new Date(match.tooltipInfo.matchDate).toLocaleDateString(
- 'en-US',
- dateFormatOptions
- )
- );
- dataForGraph = ratingStats.map((match) => match.ratingAfter.toFixed(0));
- }
-
- /* if (rankChart) {
- labels = rankChart.map((match) => match.matchName);
- dataForGraph = rankChart.map((match) => match.rank.toFixed(0));
- } */
-
- const data = {
- labels,
- datasets: [
- {
- fill: true,
- label: '',
- data: dataForGraph,
- borderWidth: 3,
- borderColor: `hsla(${colors[0]}, 0.6)`,
- backgroundColor: 'transparent' /* `hsla(${colors[1]}, 0.6)` */,
- font: font,
- },
- ],
- };
-
return (
diff --git a/components/Charts/BarChart/BarChart.tsx b/components/Charts/BarChart/BarChart.tsx
index 8d199cb..67a3e7b 100644
--- a/components/Charts/BarChart/BarChart.tsx
+++ b/components/Charts/BarChart/BarChart.tsx
@@ -48,6 +48,41 @@ export default function BarChart({
);
}, []);
+ var labels = ['HR', 'NM', 'HD', 'FM'];
+ var dataScores: any[] = [];
+
+ if (bestTournamentPerformances) {
+ labels.length = 0;
+ bestTournamentPerformances.map((tournament: any, index: any) => {
+ labels[index] = tournament.tournamentName;
+ dataScores[index] = tournament.matchCost.toFixed(2);
+ return;
+ });
+ dataScores.sort((a, b) => b - a);
+ }
+
+ if (worstTournamentPerformances) {
+ labels.length = 0;
+ worstTournamentPerformances.map((tournament: any, index: any) => {
+ labels[index] = tournament.tournamentName;
+ dataScores[index] = tournament.matchCost.toFixed(2);
+ return;
+ });
+ dataScores.sort((a, b) => b - a);
+ }
+
+ 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 options = {
indexAxis: mainAxe,
elements: {
@@ -76,11 +111,19 @@ export default function BarChart({
size: 12,
family: font,
},
+ precision: 1,
+ stepSize: 0.2,
},
grace: '2%',
- /* border: {
- color: 'transparent',
- }, */
+ min:
+ bestTournamentPerformances || worstTournamentPerformances
+ ? 0.5
+ : null,
+ max:
+ bestTournamentPerformances || worstTournamentPerformances
+ ? +dataScores[0]
+ : null,
+ suggestedMax: 2,
},
y: {
ticks: {
@@ -88,46 +131,13 @@ export default function BarChart({
size: 12,
family: font,
},
+ precision: 0,
+ stepSize: 1,
},
- grace: '10%',
- stepsSize: 1,
},
},
};
- 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: [
diff --git a/components/Charts/InlineChart/InlineChart.tsx b/components/Charts/InlineChart/InlineChart.tsx
index f866863..0417a1a 100644
--- a/components/Charts/InlineChart/InlineChart.tsx
+++ b/components/Charts/InlineChart/InlineChart.tsx
@@ -1,17 +1,18 @@
'use client';
-import { useState } from 'react';
import styles from './InlineChart.module.css';
export default function InlineChart({ matchesData }: { matchesData?: {} }) {
- const wonPercentage =
+ const wonPercentage = (
((matchesData.matchesPlayed - matchesData.matchesLost) /
matchesData.matchesPlayed) *
- 100;
- const lostPercentage =
+ 100
+ ).toFixed(1);
+ const lostPercentage = (
((matchesData.matchesPlayed - matchesData.matchesWon) /
matchesData.matchesPlayed) *
- 100;
+ 100
+ ).toFixed(1);
return (
diff --git a/components/Charts/PlayersBarChart/PlayersBarChart.tsx b/components/Charts/PlayersBarChart/PlayersBarChart.tsx
index ba375cc..29d6f56 100644
--- a/components/Charts/PlayersBarChart/PlayersBarChart.tsx
+++ b/components/Charts/PlayersBarChart/PlayersBarChart.tsx
@@ -22,7 +22,7 @@ ChartJS.register(
Legend
);
-export default function PlayersBarChart() {
+export default function PlayersBarChart({ players }: { players: [] }) {
const [font, setFont] = useState('');
/* get variables of colors from CSS */
@@ -62,10 +62,8 @@ export default function PlayersBarChart() {
size: 12,
family: font,
},
+ precision: 0,
},
- /* border: {
- color: 'transparent',
- }, */
},
y: {
ticks: {
@@ -83,15 +81,25 @@ export default function PlayersBarChart() {
},
};
- const labels = ['Akinari', 'Stage', 'Maliszewski', 'worst hr player'];
+ let labels: string[] = [];
+ let propicIDs: number[] = [];
+ let frequency: number[] = [];
+
+ if (players) {
+ players.forEach((player) => {
+ labels.push(player.username);
+ propicIDs.push(player.osuId);
+ frequency.push(player.frequency);
+ });
+ }
const data = {
labels,
datasets: [
{
label: 'Times',
- data: labels.map(() => Math.ceil(Math.random() * 1000)),
- propicIDs: ['4001304', '8191845', '12408961', '14106450'],
+ data: frequency,
+ propicIDs: propicIDs,
backgroundColor: [
'rgba(255, 99, 132)',
'rgba(54, 162, 235)',
diff --git a/components/Collapsible/FiltersCollapsible/FiltersCollapsible.module.css b/components/Collapsible/FiltersCollapsible/FiltersCollapsible.module.css
index 3d8615c..9cec94b 100644
--- a/components/Collapsible/FiltersCollapsible/FiltersCollapsible.module.css
+++ b/components/Collapsible/FiltersCollapsible/FiltersCollapsible.module.css
@@ -21,6 +21,7 @@
width: 16vw;
gap: 1rem;
flex-grow: 1;
+ flex-basis: 21%;
padding-bottom: 0.1rem;
}
diff --git a/components/Collapsible/FiltersCollapsible/FiltersCollapsible.tsx b/components/Collapsible/FiltersCollapsible/FiltersCollapsible.tsx
index cc75025..7fc1e65 100644
--- a/components/Collapsible/FiltersCollapsible/FiltersCollapsible.tsx
+++ b/components/Collapsible/FiltersCollapsible/FiltersCollapsible.tsx
@@ -34,8 +34,22 @@ export default function FiltersCollapsible({
useEffect(() => {
setParamsToPush({
...params,
- inclTier: inclTier != null ? inclTier : [],
- exclTier: exclTier != null ? exclTier : [],
+ inclTier:
+ inclTier == null
+ ? []
+ : typeof inclTier === 'string'
+ ? [inclTier]
+ : typeof inclTier === 'object'
+ ? inclTier
+ : [],
+ exclTier:
+ exclTier == null
+ ? []
+ : typeof exclTier === 'string'
+ ? [exclTier]
+ : typeof exclTier === 'object'
+ ? exclTier
+ : [],
rank: rank != null ? rank : [],
rating: rating != null ? rating : [],
matches: matches != null ? matches : [],
@@ -140,13 +154,13 @@ export default function FiltersCollapsible({
setParamsToPush={setParamsToPush}
/>
- {/*
+
Tier
- */}
+
+
{data.matchesPlayed}
diff --git a/components/Dashboard/StatsGrid/StatsGrid.module.css b/components/Dashboard/StatsGrid/StatsGrid.module.css
index d683e8a..c44fede 100644
--- a/components/Dashboard/StatsGrid/StatsGrid.module.css
+++ b/components/Dashboard/StatsGrid/StatsGrid.module.css
@@ -3,7 +3,3 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--internal-gap);
}
-
-.grid > :first-child {
- grid-column: 1 / 4;
-}
diff --git a/components/Dashboard/UserMainCard/UserMainCard.module.css b/components/Dashboard/UserMainCard/UserMainCard.module.css
index 488d3d9..8e3f445 100644
--- a/components/Dashboard/UserMainCard/UserMainCard.module.css
+++ b/components/Dashboard/UserMainCard/UserMainCard.module.css
@@ -29,6 +29,7 @@
.tierImage img {
object-fit: contain;
+ background: linear-gradient(225deg, #4c94ff 0%, #216bda 100%);
}
.username {
@@ -53,6 +54,11 @@
.rankings .header {
font-size: 3.75rem;
font-weight: 600;
+ display: flex;
+ flex-flow: row;
+ gap: 0 1rem;
+ align-items: center;
+ height: fit-content;
}
.itemsRow {
diff --git a/components/Dashboard/UserMainCard/UserMainCard.tsx b/components/Dashboard/UserMainCard/UserMainCard.tsx
index 384153d..0cf5d77 100644
--- a/components/Dashboard/UserMainCard/UserMainCard.tsx
+++ b/components/Dashboard/UserMainCard/UserMainCard.tsx
@@ -1,7 +1,5 @@
'use client';
-import { faAngleUp } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import clsx from 'clsx';
+import ProvisionalBadge from '@/components/Badges/Provisional/ProvisionalBadge';
import Image from 'next/image';
import UserRatingProgressBar from '../UserRatingProgressBar/UserRatingProgressBar';
import styles from './UserMainCard.module.css';
@@ -11,17 +9,13 @@ export default function UserMainCard({ data }: { data: {} }) {
- {data.tier}
+ {data.rankProgress.currentTier}
+ {data.isProvisional &&
}
@@ -57,7 +51,7 @@ export default function UserMainCard({ data }: { data: {} }) {
)}%`}
-
+
);
diff --git a/components/Dashboard/UserRatingProgressBar/UserRatingProgressBar.module.css b/components/Dashboard/UserRatingProgressBar/UserRatingProgressBar.module.css
index d7d76a0..34e9170 100644
--- a/components/Dashboard/UserRatingProgressBar/UserRatingProgressBar.module.css
+++ b/components/Dashboard/UserRatingProgressBar/UserRatingProgressBar.module.css
@@ -2,41 +2,67 @@
width: 100%;
margin-top: auto;
display: flex;
- flex-flow: row wrap;
- gap: 1.4rem;
- align-items: center;
+ flex-flow: column;
+ gap: 0.6em 0;
}
-.progressBar {
+.tiers {
+ display: flex;
+ flex-flow: row;
width: 100%;
- height: 15px;
- background-color: hsl(var(--gray-200));
- border-radius: 50vw;
- overflow: hidden;
+ gap: 1.4em;
+ align-items: center;
+}
+
+.tier {
+ width: 16em;
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ justify-content: center;
position: relative;
- flex: 2;
}
-.progressBar .splits {
- width: 100%;
+.tier::before {
+ font-weight: 400;
position: absolute;
- inset: 0;
- display: flex;
- flex-flow: row;
- justify-content: space-evenly;
+ bottom: 0.6em;
+ font-size: 1.6em;
+ color: hsla(var(--tier-bar-background));
}
-.splits .split {
- width: 2px;
- height: 100%;
- background-color: hsla(var(--gray-100));
- z-index: 2;
+.tier:first-child:before {
+ content: 'III';
+}
+
+.tier:nth-child(2):before {
+ content: 'II';
}
-.progressBar .currentProgress {
+.tier:nth-child(3):before {
+ content: 'I';
+}
+
+.tier .bar {
+ width: 100%;
+ height: 0.5em;
+ border-radius: 50vh;
+ overflow: hidden;
+ background-color: hsla(var(--tier-bar-background), 0.4);
+}
+
+.bar .currentProgress {
height: 100%;
- border-radius: 50vw;
- background-color: hsla(var(--blue-300));
+ border-radius: 50vh;
+ background-color: hsla(var(--tier-bar-accent));
+}
+
+.nextRank {
+ width: 2em;
+ height: 2em;
+ aspect-ratio: 1;
+ background: linear-gradient(225deg, #4c94ff 0%, #216bda 100%);
+ border-radius: 50vh;
}
.container .text {
diff --git a/components/Dashboard/UserRatingProgressBar/UserRatingProgressBar.tsx b/components/Dashboard/UserRatingProgressBar/UserRatingProgressBar.tsx
index 915e2fa..c77af7b 100644
--- a/components/Dashboard/UserRatingProgressBar/UserRatingProgressBar.tsx
+++ b/components/Dashboard/UserRatingProgressBar/UserRatingProgressBar.tsx
@@ -1,26 +1,47 @@
'use client';
import styles from './UserRatingProgressBar.module.css';
+const currentTierNumber = (current) => {
+ if (current === 3) {
+ return 1;
+ }
+ if (current === 2) {
+ return 2;
+ }
+ if (current === 1) {
+ return 3;
+ }
+};
+
export default function UserRatingProgressBar({ data }: { data: {} }) {
- const currentXP =
- ((data.ratingDelta - data.ratingForNextTier) / data.ratingDelta) * 100;
+ const currentTier = currentTierNumber(data.currentSubTier);
return (
-
-
- {[...Array(9)].map((_, index) => (
-
- ))}
-
-
+
+ {[...Array(3)].map((_, index) => (
+
+
+
currentTier
+ ? { width: '0%' }
+ : index + 1 < currentTier
+ ? { width: `100%` }
+ : null
+ }
+ />
+
+
+ ))}
+
- {data.ratingForNextTier} TR left until{' '}
- {data.nextTier}
+ {data.ratingForNextMajorTier.toFixed(0)} TR until{' '}
+ {data.nextMajorTier}
);
diff --git a/components/NavBar/NavBar.tsx b/components/NavBar/NavBar.tsx
index 5e9f50c..d3d26bc 100644
--- a/components/NavBar/NavBar.tsx
+++ b/components/NavBar/NavBar.tsx
@@ -1,4 +1,3 @@
-import { getUserData } from '@/app/actions';
import moonSVG from '@/public/icons/moon.svg';
import logo from '@/public/logos/small.svg';
import { cookies } from 'next/headers';
@@ -8,10 +7,10 @@ import HamburgerMobile from './HamburgerMobile/HamburgerMobile';
import ModeSwitcher from './ModeSwitcher/ModeSwitcher';
import styles from './NavBar.module.css';
import Routes from './Routes/Routes';
+import UserLogged from './UserLogged/UserLogged';
-export default async function NavBar() {
+export default function NavBar() {
const cookieMode = cookies().get('OTR-user-selected-osu-mode');
- const user = await getUserData();
return (
- {!user?.error && (
-
-
-
- )}
+
{/* Hamburger Menu Icon for mobile */}
diff --git a/components/NavBar/UserLogged/UserLogged.tsx b/components/NavBar/UserLogged/UserLogged.tsx
new file mode 100644
index 0000000..f5cae8f
--- /dev/null
+++ b/components/NavBar/UserLogged/UserLogged.tsx
@@ -0,0 +1,20 @@
+'use client';
+
+import { useUser } from '@/util/hooks';
+import Image from 'next/image';
+import styles from '../NavBar.module.css';
+
+export default function UserLogged() {
+ const user = useUser();
+
+ if (user?.osuId)
+ return (
+
+
+
+ );
+}
diff --git a/components/Profile/UserMainCard/UserMainCard.module.css b/components/Profile/UserMainCard/UserMainCard.module.css
index 0dfc874..f49671a 100644
--- a/components/Profile/UserMainCard/UserMainCard.module.css
+++ b/components/Profile/UserMainCard/UserMainCard.module.css
@@ -22,8 +22,8 @@
.propic {
position: relative;
- height: 60px;
- width: 60px;
+ height: 4.3rem;
+ width: 4.3rem;
aspect-ratio: 1;
border-radius: 50%;
overflow: hidden;
@@ -34,7 +34,7 @@
}
.username {
- font-size: 2.7rem;
+ font-size: 3rem;
font-weight: 600;
letter-spacing: -0.02em;
text-align: left;
@@ -54,7 +54,7 @@
display: flex;
flex-flow: column;
align-items: flex-start;
- font-size: 1.15rem;
+ font-size: 1.22rem;
gap: 0.2rem;
}
diff --git a/components/Profile/UserMainCard/UserMainCard.tsx b/components/Profile/UserMainCard/UserMainCard.tsx
index 53888cd..65a7569 100644
--- a/components/Profile/UserMainCard/UserMainCard.tsx
+++ b/components/Profile/UserMainCard/UserMainCard.tsx
@@ -2,39 +2,63 @@
import Image from 'next/image';
import styles from './UserMainCard.module.css';
-export default function UserMainCardProfile() {
+export default function UserMainCardProfile({
+ generalStats,
+ playerInfo,
+}: {
+ generalStats: object;
+ playerInfo: object;
+}) {
return (
-
Akinari
+
{playerInfo?.username}
Rating
-
1748
+
+ {Math.round(generalStats?.rating)}
+ {/* */}
+
Global
-
#4,037
+
+ {`#${generalStats?.globalRank.toLocaleString('en-US')}`}
+ {/* */}
+
Country
-
#47
+
{`#${generalStats?.countryRank.toLocaleString('en-US')}`}
Percentile
-
46.951%
+
{`${(
+ generalStats?.percentile * 100
+ ).toFixed(1)}%`}
Tier
-
Bronze
+
+ {generalStats?.rankProgress.currentTier}
+
diff --git a/components/Range/RangeSlider.tsx b/components/Range/RangeSlider.tsx
index 4159ce3..9c3e9db 100644
--- a/components/Range/RangeSlider.tsx
+++ b/components/Range/RangeSlider.tsx
@@ -1,5 +1,5 @@
'use client';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
import { Range, getTrackBackground } from 'react-range';
import styles from './RangeSlider.module.css';
@@ -45,19 +45,17 @@ export default function RangeSlider({
if (array[1] > max) {
array[1] = max;
}
- /* if (array[0] < 0) {
- array[0] = array[0] * 100;
- }
- if (array[1] < 0) {
- array[1] = array[1] * 100;
- } */
}
return array;
}
- const [values, setValues] = useState(
- value != null ? checkValues(value) : [min, max]
- );
+ const [values, setValues] = useState([min, max]);
+
+ useEffect(() => {
+ setValues(value !== undefined ? checkValues(value) : [min, max]);
+
+ return () => {};
+ }, [value]);
return (
@@ -147,7 +145,7 @@ export default function RangeSlider({
values,
colors: [
'hsla(var(--gray-600))',
- 'hsla(var(--gray-600))',
+ 'hsla(var(--accent-secondary-color))',
'hsla(var(--gray-600))',
],
min: min,
diff --git a/components/TierSelector/TierSelector.tsx b/components/TierSelector/TierSelector.tsx
index 73b152f..6d73ba4 100644
--- a/components/TierSelector/TierSelector.tsx
+++ b/components/TierSelector/TierSelector.tsx
@@ -44,6 +44,11 @@ const possibleRanks = [
},
];
+function includesMatch(array: any, match: string) {
+ const result = array.filter((item: any) => item === match);
+ return result == match;
+}
+
export default function TierSelector({
value,
setParamsToPush,
@@ -55,55 +60,67 @@ export default function TierSelector({
const [excludedRanks, setExcludedRanks] = useState([]);
const selectRank = async (rank: string) => {
- if (!ranks.includes(rank) && !excludedRanks.includes(rank)) {
+ if (!includesMatch(ranks, rank) && !includesMatch(excludedRanks, rank)) {
setRanks((prev: any) => [...prev, rank]);
return setParamsToPush((prev: any) => ({
...prev,
inclTier: [...prev.inclTier, rank],
}));
}
- if (ranks.includes(rank)) {
+ if (includesMatch(ranks, rank)) {
setExcludedRanks((prev: any) => [...prev, rank]);
setParamsToPush((prev: any) => ({
...prev,
exclTier: [...prev.exclTier, rank],
}));
setRanks((prev: any) => prev.filter((item: any) => item !== rank));
- return setParamsToPush((prev: any) => ({
- ...prev,
- inclTier: [
- ...prev.inclTier.slice(
- 0,
- prev.inclTier.findIndex((name) => name === rank)
- ),
- ...prev.inclTier.slice(
- prev.inclTier.findIndex((name) => name === rank) + 1
- ),
- ],
- }));
+ return setParamsToPush((prev: any) => {
+ return {
+ ...prev,
+ inclTier: [
+ ...prev.inclTier.slice(
+ 0,
+ prev.inclTier.findIndex((name) => name === rank)
+ ),
+ ...prev.inclTier.slice(
+ prev.inclTier.findIndex((name) => name === rank) + 1
+ ),
+ ],
+ };
+ });
}
- if (excludedRanks.includes(rank)) {
+ if (includesMatch(excludedRanks, rank)) {
setExcludedRanks((prev: any) =>
prev.filter((item: any) => item !== rank)
);
- return setParamsToPush((prev: any) => ({
- ...prev,
- exclTier: [
- ...prev.exclTier.slice(
- 0,
- prev.exclTier.findIndex((name) => name === rank)
- ),
- ...prev.exclTier.slice(
- prev.exclTier.findIndex((name) => name === rank) + 1
- ),
- ],
- }));
+ return setParamsToPush((prev: any) => {
+ return {
+ ...prev,
+ exclTier: [
+ ...prev.exclTier.slice(
+ 0,
+ prev.exclTier.findIndex((name) => name === rank)
+ ),
+ ...prev.exclTier.slice(
+ prev.exclTier.findIndex((name) => name === rank) + 1
+ ),
+ ],
+ };
+ });
}
};
useEffect(() => {
- setRanks(value?.inclTier ?? []);
- setExcludedRanks(value?.exclTier ?? []);
+ setRanks(
+ typeof value?.inclTier === 'string'
+ ? [value?.inclTier]
+ : value?.inclTier ?? []
+ );
+ setExcludedRanks(
+ typeof value?.exclTier === 'string'
+ ? [value?.exclTier]
+ : value?.exclTier ?? []
+ );
}, [value.inclTier, value.exclTier]);
return (
@@ -120,16 +137,16 @@ export default function TierSelector({
- {ranks.includes(rank.id) ? (
+ {includesMatch(ranks, rank.id) ? (
- ) : excludedRanks.includes(rank.id) ? (
+ ) : includesMatch(excludedRanks, rank.id) ? (
) : (
''
diff --git a/lib/types.ts b/lib/types.ts
index 1bd52b0..a80a2e2 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -50,3 +50,15 @@ export const MatchesSubmitFormSchema = z.object({
)
.min(1),
});
+
+export interface User {
+ "id": number,
+ "userId": number,
+ "osuId": number,
+ "osuCountry": string,
+ "osuPlayMode": number,
+ "username": string,
+ "roles": [
+ string
+ ]
+}
diff --git a/middleware.ts b/middleware.ts
index f6e75fa..26a5741 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -11,7 +11,7 @@ export async function middleware(request: NextRequest) {
request.nextUrl.pathname.startsWith('/submit')) &&
user?.error
) {
- return NextResponse.rewrite(new URL('/', request.url));
+ return NextResponse.redirect(new URL('/', request.url));
}
}
diff --git a/public/icons/provisional.png b/public/icons/provisional.png
new file mode 100644
index 0000000..3f1e7ab
Binary files /dev/null and b/public/icons/provisional.png differ
diff --git a/util/UserLoggedContext.tsx b/util/UserLoggedContext.tsx
new file mode 100644
index 0000000..ce6d9b0
--- /dev/null
+++ b/util/UserLoggedContext.tsx
@@ -0,0 +1,41 @@
+'use client';
+import { checkUserLogin } from '@/app/actions';
+import { User } from '@/lib/types';
+import {
+ DependencyList,
+ createContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import type { ReactNode } from 'react';
+
+export const UserLoggedContext = createContext