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"