+>(({ className, ...props }, ref) => (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+))
+TableCell.displayName = "TableCell"
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableCaption.displayName = "TableCaption"
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
new file mode 100644
index 0000000..0f4caeb
--- /dev/null
+++ b/components/ui/tabs.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts
index a4e3ff9..7918494 100644
--- a/convex/_generated/api.d.ts
+++ b/convex/_generated/api.d.ts
@@ -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";
@@ -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;
diff --git a/convex/leaderboard.ts b/convex/leaderboard.ts
new file mode 100644
index 0000000..5f35ca3
--- /dev/null
+++ b/convex/leaderboard.ts
@@ -0,0 +1,86 @@
+import { v } from "convex/values";
+import { internalMutation, query } from "./_generated/server";
+
+export const getGlobalRankings = query({
+ handler: async ({ db }) => {
+ const res = await db.query("globalrankings").collect();
+ // Sort the results by wins/losses ratio
+
+ const sortedResults = res.sort((a, b) => {
+ if (a.wins / (a.wins + a.losses) > b.wins / (b.wins + b.losses)) {
+ return -1;
+ }
+ if (a.wins / (a.wins + a.losses) < b.wins / (b.wins + b.losses)) {
+ return 1;
+ }
+ return 0;
+ });
+
+ return sortedResults;
+ },
+});
+
+export const getLevelRankings = query({
+ handler: async ({ db }) => {
+ const res = await db.query("levelrankings").collect();
+
+ const sortedResults = res.sort((a, b) => {
+ if (a.level < b.level) {
+ return -1;
+ }
+ if (a.level > b.level) {
+ return 1;
+ }
+ return 0;
+ });
+
+ return sortedResults;
+ },
+});
+
+export const updateRankings = internalMutation({
+ args: {
+ modelId: v.string(),
+ level: v.number(),
+ isWin: v.boolean(),
+ },
+ handler: async (ctx, args) => {
+ const globalRanking = await ctx.db
+ .query("globalrankings")
+ .withIndex("by_modelId", (q) => q.eq("modelId", args.modelId))
+ .collect();
+ const levelRanking = await ctx.db
+ .query("levelrankings")
+ .withIndex("by_modelId_level", (q) =>
+ q.eq("modelId", args.modelId).eq("level", args.level),
+ )
+ .collect();
+
+ if (globalRanking.length === 0) {
+ await ctx.db.insert("globalrankings", {
+ modelId: args.modelId,
+ wins: args.isWin ? 1 : 0,
+ losses: args.isWin ? 0 : 1,
+ });
+ } else {
+ await ctx.db.patch(globalRanking[0]._id, {
+ wins: globalRanking[0].wins + (args.isWin ? 1 : 0),
+ losses: globalRanking[0].losses + (args.isWin ? 0 : 1),
+ });
+ }
+
+ if (levelRanking.length === 0) {
+ await ctx.db.insert("levelrankings", {
+ modelId: args.modelId,
+ level: args.level,
+ wins: args.isWin ? 1 : 0,
+ losses: args.isWin ? 0 : 1,
+ });
+ } else {
+ await ctx.db.patch(levelRanking[0]._id, {
+ wins: levelRanking[0].wins + (args.isWin ? 1 : 0),
+ losses: levelRanking[0].losses + (args.isWin ? 0 : 1),
+ });
+ }
+ },
+});
diff --git a/convex/maps.ts b/convex/maps.ts
index a2ccce2..40a4465 100644
--- a/convex/maps.ts
+++ b/convex/maps.ts
@@ -188,6 +188,12 @@ export const playMapAction = internalAction({
resultId,
error: errorMessage,
});
+
+ await ctx.runMutation(internal.leaderboard.updateRankings, {
+ modelId: args.modelId,
+ level: args.level,
+ isWin: false,
+ });
}
},
});
diff --git a/convex/results.ts b/convex/results.ts
index d9eb5cf..c66db40 100644
--- a/convex/results.ts
+++ b/convex/results.ts
@@ -102,6 +102,12 @@ export const updateResult = internalMutation({
throw new Error("Game not found");
}
+ await ctx.runMutation(internal.leaderboard.updateRankings, {
+ modelId: game.modelId,
+ level: result.level,
+ isWin: args.isWin,
+ });
+
if (args.isWin) {
await ctx.runMutation(internal.scores.incrementScore, {
modelId: game.modelId,
diff --git a/convex/schema.ts b/convex/schema.ts
index 2741e80..7f950f1 100644
--- a/convex/schema.ts
+++ b/convex/schema.ts
@@ -32,4 +32,15 @@ export default defineSchema({
v.literal("failed"),
),
}).index("by_gameId_level", ["gameId", "level"]),
+ globalrankings: defineTable({
+ modelId: v.string(),
+ wins: v.number(),
+ losses: v.number(),
+ }).index("by_modelId", ["modelId"]),
+ levelrankings: defineTable({
+ modelId: v.string(),
+ level: v.number(),
+ wins: v.number(),
+ losses: v.number(),
+ }).index("by_modelId_level", ["modelId", "level"]),
});
diff --git a/package.json b/package.json
index 68ac54b..4322f2f 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
|