diff --git a/src/app/player/[id]/page.tsx b/src/app/player/[id]/page.tsx index 9864b23..1b2c166 100644 --- a/src/app/player/[id]/page.tsx +++ b/src/app/player/[id]/page.tsx @@ -5,6 +5,7 @@ import Card from "@/components/Card"; import Container from "@/components/Container"; import Label from "@/components/Label"; import Pagination from "@/components/Pagination"; +import PlayerChart from "@/components/PlayerChart"; import ScoreStatLabel from "@/components/ScoreStatLabel"; import { Spinner } from "@/components/Spinner"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; @@ -135,7 +136,7 @@ export default function Player({ params }: { params: { id: string } }) { -
+

{playerData.name}

@@ -184,6 +185,8 @@ export default function Player({ params }: { params: { id: string } }) { value={formatNumber(playerData.scoreStats.replaysWatched)} />
+ +
diff --git a/src/components/PlayerChart.tsx b/src/components/PlayerChart.tsx new file mode 100644 index 0000000..b7e97fb --- /dev/null +++ b/src/components/PlayerChart.tsx @@ -0,0 +1,121 @@ +import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; +import { formatNumber } from "@/utils/number"; +import { + CategoryScale, + Chart as ChartJS, + Legend, + LineElement, + LinearScale, + PointElement, + Title, + Tooltip, +} from "chart.js"; +import { Line } from "react-chartjs-2"; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +); + +type PlayerChartProps = { + className?: string; + scoresaber: ScoresaberPlayer; +}; + +export const options: any = { + maintainAspectRatio: false, + aspectRatio: 1, + interaction: { + mode: "index", + intersect: false, + }, + scales: { + y: { + ticks: { + autoSkip: true, + maxTicksLimit: 4, + }, + reverse: true, + }, + x: { + ticks: { + autoSkip: true, + }, + }, + }, + elements: { + point: { + radius: 0, + }, + }, + plugins: { + legend: { + position: "top" as const, + labels: { + color: "white", + }, + }, + title: { + display: false, + }, + tooltip: { + callbacks: { + label(context: any) { + switch (context.dataset.label) { + case "Rank": { + return `Rank #${formatNumber(context.parsed.y.toFixed(0))}`; + } + } + }, + }, + }, + }, +}; + +export default function PlayerChart({ + className, + scoresaber, +}: PlayerChartProps) { + const history: number[] = scoresaber.histories + .split(",") + .map(function (item) { + return parseInt(item); + }); + + let labels = []; + for (let i = history.length; i > 0; i--) { + let label = `${i} days ago`; + if (i === 1) { + label = "now"; + } + if (i === 2) { + label = "yesterday"; + } + labels.push(label); + } + + const data = { + labels, + datasets: [ + { + lineTension: 0.4, + data: history, + label: "Rank", + borderColor: "#3e95cd", + fill: false, + color: "#fff", + }, + ], + }; + + return ( +
+ +
+ ); +} diff --git a/src/components/SearchPlayer.tsx b/src/components/SearchPlayer.tsx index 15013b8..2f634b2 100644 --- a/src/components/SearchPlayer.tsx +++ b/src/components/SearchPlayer.tsx @@ -22,6 +22,7 @@ export default function SearchPlayer() { }, [search]); async function searchPlayer(search: string) { + // Check if the search is a profile link if (search.startsWith("https://scoresaber.com/u/")) { const id = search.split("/").pop(); if (id == undefined) return; @@ -31,6 +32,8 @@ export default function SearchPlayer() { setPlayers([player]); } + + // Search by name const players = await searchByName(search); if (players == undefined) return;