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;