Files
rate-my-shots/src/components/stats/matches-table.tsx
2026-05-03 17:07:05 +01:00

147 lines
4.6 KiB
TypeScript

"use client";
import { Button, Table } from "@heroui/react";
import { buttonVariants } from "@heroui/styles";
import Link from "next/link";
import useSWRInfinite from "swr/infinite";
import { TimeAgo } from "@/components/time-ago";
import { formatElo } from "@/lib/format";
import type { MatchEntry } from "@/lib/stats";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function MatchRows({ matches }: { matches: MatchEntry[] }) {
return (
<>
{matches.map((match) => (
<Table.Row key={match.id}>
<Table.Cell className="text-center">
<Link
href={`/assets/${match.leftId}`}
className="inline-flex justify-center"
>
<img
src={`/img/${match.leftId}`}
alt=""
className="h-8 rounded object-contain"
style={{ aspectRatio: match.leftAspectRatio }}
/>
</Link>
</Table.Cell>
<Table.Cell
className={`text-center text-xs tabular-nums font-semibold ${match.winningSide === "left" ? "text-success" : "text-danger"}`}
>
{formatElo(match.leftEloAfter)}
</Table.Cell>
<Table.Cell className="text-center tabular-nums text-sm text-muted">
±{match.eloDelta.toFixed(1)}
</Table.Cell>
<Table.Cell
className={`text-center text-xs tabular-nums font-semibold ${match.winningSide === "right" ? "text-success" : "text-danger"}`}
>
{formatElo(match.rightEloAfter)}
</Table.Cell>
<Table.Cell className="text-center">
<Link
href={`/assets/${match.rightId}`}
className="inline-flex justify-center"
>
<img
src={`/img/${match.rightId}`}
alt=""
className="h-8 rounded object-contain"
style={{ aspectRatio: match.rightAspectRatio }}
/>
</Link>
</Table.Cell>
<Table.Cell>
<TimeAgo date={match.createdAt} />
</Table.Cell>
</Table.Row>
))}
</>
);
}
function MatchesTableHeader() {
return (
<Table.Header>
<Table.Column isRowHeader className="text-center">
Left
</Table.Column>
<Table.Column className="text-center tabular-nums">ELO</Table.Column>
<Table.Column className="text-center">±</Table.Column>
<Table.Column className="text-center tabular-nums">ELO</Table.Column>
<Table.Column className="text-center">Right</Table.Column>
<Table.Column>When</Table.Column>
</Table.Header>
);
}
export function MatchesPreview({ data }: { data: MatchEntry[] }) {
return (
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-muted uppercase tracking-widest">
Recent Matches
</h3>
<Table>
<Table.ScrollContainer>
<Table.Content aria-label="Recent matches">
<MatchesTableHeader />
<Table.Body>
<MatchRows matches={data} />
</Table.Body>
</Table.Content>
</Table.ScrollContainer>
</Table>
<div className="flex justify-center">
<Link
href="/stats/matches"
className={buttonVariants({ variant: "secondary" })}
>
See more
</Link>
</div>
</div>
);
}
export function MatchesTable({ initialData }: { initialData: MatchEntry[] }) {
const { data, size, setSize, isValidating } = useSWRInfinite(
(index) => `/api/stats/matches?page=${index}`,
fetcher,
{ fallbackData: [initialData], revalidateFirstPage: false },
);
const allMatches: MatchEntry[] = data ? data.flat() : [];
const hasMore = data ? data[data.length - 1]?.length === 20 : false;
const isLoadingMore =
isValidating && data && typeof data[size - 1] === "undefined";
return (
<div className="flex flex-col gap-4">
<Table>
<Table.ScrollContainer>
<Table.Content aria-label="Recent matches">
<MatchesTableHeader />
<Table.Body>
<MatchRows matches={allMatches} />
</Table.Body>
</Table.Content>
</Table.ScrollContainer>
</Table>
{hasMore && (
<div className="flex justify-center">
<Button
variant="secondary"
onPress={() => setSize(size + 1)}
isDisabled={isLoadingMore}
>
{isLoadingMore ? "Loading..." : "Load more"}
</Button>
</div>
)}
</div>
);
}