leaderboard and ranked systems

This commit is contained in:
2026-07-11 11:13:28 -04:00
commit 02ac5f9593
44 changed files with 12318 additions and 0 deletions
@@ -0,0 +1,102 @@
-- CreateEnum
CREATE TYPE "Subject" AS ENUM ('MATH', 'READING', 'WRITING');
-- CreateEnum
CREATE TYPE "Difficulty" AS ENUM ('EASY', 'MEDIUM', 'HARD');
-- CreateEnum
CREATE TYPE "SessionStatus" AS ENUM ('IN_PROGRESS', 'COMPLETED', 'ABANDONED');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"googleId" TEXT NOT NULL,
"email" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"avatarUrl" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Question" (
"id" TEXT NOT NULL,
"grade" INTEGER NOT NULL,
"subject" "Subject" NOT NULL,
"strand" TEXT NOT NULL,
"difficulty" "Difficulty" NOT NULL,
"questionText" TEXT NOT NULL,
"options" JSONB NOT NULL,
"correctAnswer" TEXT NOT NULL,
"explanation" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Question_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GameSession" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"grade" INTEGER NOT NULL,
"subject" "Subject" NOT NULL,
"status" "SessionStatus" NOT NULL DEFAULT 'IN_PROGRESS',
"score" INTEGER NOT NULL DEFAULT 0,
"totalQuestions" INTEGER NOT NULL,
"correctAnswers" INTEGER NOT NULL DEFAULT 0,
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
CONSTRAINT "GameSession_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserAnswer" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"questionId" TEXT NOT NULL,
"selectedAnswer" TEXT NOT NULL,
"isCorrect" BOOLEAN NOT NULL,
"timeSpentMs" INTEGER NOT NULL,
"answeredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UserAnswer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LeaderboardEntry" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"grade" INTEGER NOT NULL,
"subject" "Subject" NOT NULL,
"bestScore" INTEGER NOT NULL,
"gamesPlayed" INTEGER NOT NULL,
"totalCorrect" INTEGER NOT NULL,
"rank" INTEGER,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "LeaderboardEntry_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_googleId_key" ON "User"("googleId");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "LeaderboardEntry_userId_grade_subject_key" ON "LeaderboardEntry"("userId", "grade", "subject");
-- AddForeignKey
ALTER TABLE "GameSession" ADD CONSTRAINT "GameSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserAnswer" ADD CONSTRAINT "UserAnswer_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "GameSession"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserAnswer" ADD CONSTRAINT "UserAnswer_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "Question"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LeaderboardEntry" ADD CONSTRAINT "LeaderboardEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,25 @@
/*
Warnings:
- Changed the type of `grade` on the `GameSession` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Changed the type of `grade` on the `LeaderboardEntry` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Changed the type of `grade` on the `Question` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- CreateEnum
CREATE TYPE "Grade" AS ENUM ('G3', 'G6', 'G9');
-- AlterTable
ALTER TABLE "GameSession" DROP COLUMN "grade",
ADD COLUMN "grade" "Grade" NOT NULL;
-- AlterTable
ALTER TABLE "LeaderboardEntry" DROP COLUMN "grade",
ADD COLUMN "grade" "Grade" NOT NULL;
-- AlterTable
ALTER TABLE "Question" DROP COLUMN "grade",
ADD COLUMN "grade" "Grade" NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "LeaderboardEntry_userId_grade_subject_key" ON "LeaderboardEntry"("userId", "grade", "subject");
@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "LeaderboardScore" (
"id" TEXT NOT NULL,
"playerName" TEXT NOT NULL,
"grade" "Grade" NOT NULL,
"score" INTEGER NOT NULL,
"totalQuestions" INTEGER NOT NULL,
"timeSeconds" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "LeaderboardScore_pkey" PRIMARY KEY ("id")
);
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "username" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
@@ -0,0 +1,26 @@
-- Drop stale table (old schema: subject, bestScore, rank — missing elo, bestTime)
DROP TABLE IF EXISTS "LeaderboardEntry";
-- Drop orphaned table from old migration
DROP TABLE IF EXISTS "LeaderboardScore";
-- Recreate with correct schema
CREATE TABLE "LeaderboardEntry" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"grade" "Grade" NOT NULL,
"elo" INTEGER NOT NULL DEFAULT 1000,
"bestTime" INTEGER,
"gamesPlayed" INTEGER NOT NULL DEFAULT 0,
"totalCorrect" INTEGER NOT NULL DEFAULT 0,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT NOW(),
CONSTRAINT "LeaderboardEntry_pkey" PRIMARY KEY ("id")
);
-- Unique per user+grade (no subject)
CREATE UNIQUE INDEX "LeaderboardEntry_userId_grade_key" ON "LeaderboardEntry"("userId", "grade");
-- Foreign key
ALTER TABLE "LeaderboardEntry" ADD CONSTRAINT "LeaderboardEntry_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1 @@
ALTER TABLE "LeaderboardEntry" ALTER COLUMN "elo" SET DEFAULT 800;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "LeaderboardEntry" ALTER COLUMN "updatedAt" DROP DEFAULT;
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
+105
View File
@@ -0,0 +1,105 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Grade {
G3
G6
G9
}
enum Subject {
MATH
READING
WRITING
}
enum Difficulty {
EASY
MEDIUM
HARD
}
enum SessionStatus {
IN_PROGRESS
COMPLETED
ABANDONED
}
model User {
id String @id @default(cuid())
googleId String @unique
email String @unique
displayName String
username String? @unique
avatarUrl String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions GameSession[]
leaderboardEntries LeaderboardEntry[]
}
model Question {
id String @id @default(cuid())
grade Grade
subject Subject
strand String
difficulty Difficulty
questionText String
options Json
correctAnswer String
explanation String?
createdAt DateTime @default(now())
answers UserAnswer[]
}
model GameSession {
id String @id @default(cuid())
userId String
grade Grade
subject Subject
status SessionStatus @default(IN_PROGRESS)
score Int @default(0)
totalQuestions Int
correctAnswers Int @default(0)
startedAt DateTime @default(now())
completedAt DateTime?
user User @relation(fields: [userId], references: [id])
answers UserAnswer[]
}
model UserAnswer {
id String @id @default(cuid())
sessionId String
questionId String
selectedAnswer String
isCorrect Boolean
timeSpentMs Int
answeredAt DateTime @default(now())
session GameSession @relation(fields: [sessionId], references: [id])
question Question @relation(fields: [questionId], references: [id])
}
model LeaderboardEntry {
id String @id @default(cuid())
userId String
grade Grade
elo Int @default(800)
bestTime Int?
gamesPlayed Int @default(0)
totalCorrect Int @default(0)
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@unique([userId, grade])
}