Skip to content

Commit

Permalink
Merge pull request #51 from Ashutoshbind15/main
Browse files Browse the repository at this point in the history
Add the live leaderboard
  • Loading branch information
webdevcody authored Oct 16, 2024
2 parents 931e177 + 9a9e846 commit 93011a4
Show file tree
Hide file tree
Showing 10 changed files with 437 additions and 0 deletions.
3 changes: 3 additions & 0 deletions app/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export default function Header() {
<Link href="/play">
<Button variant="ghost">Play</Button>
</Link>
<Link href="/leaderboard">
<Button variant="ghost">Leaderboard</Button>
</Link>
</nav>

<div className="flex items-center space-x-4">
Expand Down
147 changes: 147 additions & 0 deletions app/leaderboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"use client";

import { api } from "@/convex/_generated/api";
import { useQuery } from "convex/react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";

// Define the types for the data
interface Ranking {
_id: string;
modelId: string;
level?: string;
wins: number;
losses: number;
}

interface Stats {
wins: number;
losses: number;
total: number;
ratio: number;
}

const LeaderBoard = () => {
const globalRanking = useQuery(api.leaderboard.getGlobalRankings) as
| Ranking[]
| undefined;
const levelRanking = useQuery(api.leaderboard.getLevelRankings) as
| Ranking[]
| undefined;

// Transform the levelRanking data into a pivot table structure
const pivotLevelData = (levelRanking: Ranking[] | undefined) => {
const levels: Record<string, Record<string, Stats>> = {};

levelRanking?.forEach((item) => {
if (!levels[item.level!]) {
levels[item.level!] = {};
}

levels[item.level!][item.modelId] = {
wins: item.wins,
losses: item.losses,
total: item.wins + item.losses,
ratio: item.wins / (item.wins + item.losses),
};
});

return levels;
};

const pivotedLevelData = pivotLevelData(levelRanking);

// Get all unique model IDs to dynamically create columns
const allModels = Array.from(
new Set(levelRanking?.map((item) => item.modelId)),
);

return (
<div className="py-12">
<div className="text-center text-3xl font-semibold mb-6">Leaderboard</div>

<Tabs defaultValue="global" className="">
<TabsList>
<TabsTrigger value="global">Global Rankings</TabsTrigger>
<TabsTrigger value="level">Map based Rankings</TabsTrigger>
</TabsList>
<TabsContent value="global" className="p-4">
{/* Global Rankings Table */}
<Table>
<TableCaption>The global model realtime tally.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]">Model ID</TableHead>
<TableHead>Number of Wins</TableHead>
<TableHead>Number of Losses</TableHead>
<TableHead>Total Games</TableHead>
<TableHead className="text-right">Ratio</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{globalRanking?.map((item) => (
<TableRow key={item._id}>
<TableCell>{item.modelId}</TableCell>
<TableCell>{item.wins}</TableCell>
<TableCell>{item.losses}</TableCell>
<TableCell>{item.wins + item.losses}</TableCell>
<TableCell className="text-right">
{(item.wins / (item.wins + item.losses)).toFixed(2)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabsContent>
<TabsContent value="level" className="p-4">
{/* Map-based Rankings Pivoted Table */}
<Table>
<TableCaption>The models map-based tally per level.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Level</TableHead>
{/* Dynamically render column headers for each model */}
{allModels.map((modelId) => (
<TableHead key={modelId}>{modelId}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{/* Render rows for each level */}
{Object.entries(pivotedLevelData).map(([level, models]) => (
<TableRow key={level}>
<TableCell>{level}</TableCell>
{/* Render stats for each model in the columns */}
{allModels.map((modelId) => {
const stats = models[modelId] || {
wins: 0,
losses: 0,
total: 0,
ratio: 0,
};
return (
<TableCell key={modelId}>
Wins: {stats.wins}, Losses: {stats.losses}, Games:{" "}
{stats.total}, Ratio: {stats.ratio.toFixed(2)}
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
</TabsContent>
</Tabs>
</div>
);
};

export default LeaderBoard;
120 changes: 120 additions & 0 deletions components/ui/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as React from "react"

import { cn } from "@/lib/utils"

const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"

const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"

const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"

const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"

const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"

const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"

const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"

const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"

export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
55 changes: 55 additions & 0 deletions components/ui/tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client"

import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"

import { cn } from "@/lib/utils"

const Tabs = TabsPrimitive.Root

const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName

const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName

const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName

export { Tabs, TabsList, TabsTrigger, TabsContent }
2 changes: 2 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type * as constants from "../constants.js";
import type * as games from "../games.js";
import type * as http from "../http.js";
import type * as init from "../init.js";
import type * as leaderboard from "../leaderboard.js";
import type * as maps from "../maps.js";
import type * as results from "../results.js";
import type * as scores from "../scores.js";
Expand All @@ -39,6 +40,7 @@ declare const fullApi: ApiFromModules<{
games: typeof games;
http: typeof http;
init: typeof init;
leaderboard: typeof leaderboard;
maps: typeof maps;
results: typeof results;
scores: typeof scores;
Expand Down
Loading

0 comments on commit 93011a4

Please sign in to comment.