Skip to content

Commit

Permalink
Merge pull request #37 from RuffByte/KL-UserStats
Browse files Browse the repository at this point in the history
  • Loading branch information
tulza authored Oct 8, 2024
2 parents 2b62d1a + 7e4ef36 commit f5bcc14
Show file tree
Hide file tree
Showing 26 changed files with 1,079 additions and 75 deletions.
6 changes: 3 additions & 3 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
// removeConsole: { exclude: ['error'] },
},
// compiler: {
// removeConsole: { exclude: ['error'] },
// },
webpack: (config) => {
config.externals.push('@node-rs/argon2', '@node-rs/bcrypt');
return config;
Expand Down

This file was deleted.

93 changes: 93 additions & 0 deletions prisma/migrations/20241007100732_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "PasswordResetToken" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"role" TEXT,
"hashedPassword" TEXT,
"picture" TEXT,
"totalGames" INTEGER NOT NULL DEFAULT 0,
"totalTime" DOUBLE PRECISION NOT NULL DEFAULT 0.0,

CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "UserStats" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"mode" TEXT NOT NULL,
"category" TEXT NOT NULL,
"avgLpm" DOUBLE PRECISION NOT NULL DEFAULT 0.0,
"avgAccuracy" DOUBLE PRECISION NOT NULL DEFAULT 0.0,
"totalGames" INTEGER NOT NULL DEFAULT 0,
"totalTime" DOUBLE PRECISION NOT NULL DEFAULT 0.0,

CONSTRAINT "UserStats_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "GameEntry" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"mode" TEXT NOT NULL,
"language" TEXT NOT NULL,
"wpm" DOUBLE PRECISION NOT NULL,
"rawWpm" DOUBLE PRECISION NOT NULL,
"lpm" DOUBLE PRECISION NOT NULL,
"rawLpm" DOUBLE PRECISION NOT NULL,
"totalChar" INTEGER NOT NULL,
"totalClicks" INTEGER NOT NULL,
"totalTime" DOUBLE PRECISION NOT NULL,
"accuracy" DOUBLE PRECISION NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"targetSize" INTEGER NOT NULL,

CONSTRAINT "GameEntry_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- CreateIndex
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");

-- CreateIndex
CREATE UNIQUE INDEX "UserStats_userId_mode_category_key" ON "UserStats"("userId", "mode", "category");

-- CreateIndex
CREATE INDEX "GameEntry_lpm_userId_idx" ON "GameEntry"("lpm", "userId");

-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "UserStats" ADD CONSTRAINT "UserStats_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "GameEntry" ADD CONSTRAINT "GameEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "UserStats" ADD COLUMN "bestGameEntryId" TEXT;

-- AddForeignKey
ALTER TABLE "UserStats" ADD CONSTRAINT "UserStats_bestGameEntryId_fkey" FOREIGN KEY ("bestGameEntryId") REFERENCES "GameEntry"("id") ON DELETE SET NULL ON UPDATE CASCADE;
60 changes: 41 additions & 19 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,6 @@ datasource db {
directUrl = env("DIRECT_URL")
}

model User {
id String @id @default(cuid())
email String @unique
name String @unique
role String?
hashedPassword String?
picture String?
session Session[]
resetTokens PasswordResetToken[]
GameEntry GameEntry[]
}

model Session {
id String @id
userId String
Expand All @@ -42,22 +30,56 @@ model PasswordResetToken {
user User @relation(fields: [userId], references: [id])
}

model User {
id String @id @default(cuid())
email String @unique
name String @unique
role String?
hashedPassword String?
picture String?
session Session[]
resetTokens PasswordResetToken[]
GameEntry GameEntry[]
userStats UserStats[]
totalGames Int @default(0) // Total games played across all modes
totalTime Float @default(0.0) // Total time spent across all modes
joinedAt DateTime @default(now()) // Timestamp of when the user joined
}

//super duper mega scalable :) (im lying)
model UserStats {
id String @id @default(cuid())
userId String
mode String
category String
avgLpm Float @default(0.0)
avgAccuracy Float @default(0.0)
totalGames Int @default(0)
totalTime Float @default(0.0)
bestGameEntryId String?
bestGameEntry GameEntry? @relation(fields: [bestGameEntryId], references: [id])
user User @relation(fields: [userId], references: [id])
@@unique([userId, mode, category])
}

model GameEntry {
id String @id @default(cuid())
id String @id @default(cuid())
userId String
mode String
language String
wpm Int
rawWpm Int
lpm Int
rawLpm Int
wpm Float
rawWpm Float
lpm Float
rawLpm Float
totalChar Int
totalClicks Int
totalTime Float
accuracy Float
createdAt DateTime @default(now())
createdAt DateTime @default(now())
targetSize Int
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id])
UserStats UserStats[]
@@index([lpm, userId])
}
1 change: 1 addition & 0 deletions prisma/scripts/recalculateFromStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//this is just a function in case we manually invalidate a score and need to recalc some1s stats
7 changes: 7 additions & 0 deletions public/themes/catcopy.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
:root {
--background: 233 18% 18%; /* catppuccin clone */
--secondary: 274 71% 77%;
--foreground: 317 76% 84%;
--tertiary: #000;
--hover: 0 0% 100%;
}
9 changes: 9 additions & 0 deletions public/themes/mute.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* Some awwwards site i stole */

:root {
--background: 220 4% 13%;
--secondary: 50 100% 96%;
--foreground: 50 100% 96%;
--tertiary: 0 0% 100%;
--hover: 0 0% 100%;
}
84 changes: 80 additions & 4 deletions src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { prisma } from '@/lib/prisma';
import { GameData } from './types/gameData';

export async function submitGameData(gameData: GameData) {
// First, find the user's id based on the userName
const user = await prisma.user.findUnique({
where: {
name: gameData.userName,
Expand All @@ -18,10 +17,10 @@ export async function submitGameData(gameData: GameData) {
throw new Error(`User with username ${gameData.userName} not found`);
}

// Create the game entry using the user's id
await prisma.gameEntry.create({
// Create game entry
const gameEntry = await prisma.gameEntry.create({
data: {
userId: user.id, // Use userId now
userId: user.id,
mode: gameData.mode,
language: gameData.language,
totalChar: gameData.totalChar,
Expand All @@ -36,5 +35,82 @@ export async function submitGameData(gameData: GameData) {
},
});

// Update user's total games and total time
await prisma.user.update({
where: { id: user.id },
data: {
totalGames: { increment: 1 },
totalTime: { increment: gameData.totalTime },
},
});

// Determine the category (time or characters)
const category =
gameData.mode === 'time'
? `${gameData.totalTime}s`
: `${gameData.totalChar}c`;

// Get current user stats
const currentStats = await prisma.userStats.findUnique({
where: {
userId_mode_category: {
userId: user.id,
mode: gameData.mode,
category: category,
},
},
include: {
bestGameEntry: true, // Fetch the current best game entry
},
});

// Calculate new average LPM and accuracy
const newTotalGames = (currentStats?.totalGames || 0) + 1;
const newTotalLpm =
(currentStats?.avgLpm || 0) * (currentStats?.totalGames || 0) +
gameData.lpm;
const newTotalAccuracy =
(currentStats?.avgAccuracy || 0) * (currentStats?.totalGames || 0) +
gameData.accuracy;

const newAvgLpm = newTotalLpm / newTotalGames;
const newAvgAccuracy = newTotalAccuracy / newTotalGames;

// Check if the current game entry is the best (highest lpm)
const isBestEntry =
!currentStats?.bestGameEntry ||
gameData.lpm > currentStats.bestGameEntry.lpm;

// Upsert (create or update) user stats
await prisma.userStats.upsert({
where: {
userId_mode_category: {
userId: user.id,
mode: gameData.mode,
category: category,
},
},
update: {
avgLpm: newAvgLpm,
avgAccuracy: newAvgAccuracy,
totalGames: { increment: 1 },
totalTime: { increment: gameData.totalTime },
// Update best game entry if this one is better
bestGameEntryId: isBestEntry
? gameEntry.id
: currentStats.bestGameEntryId,
},
create: {
userId: user.id,
mode: gameData.mode,
category: category,
avgLpm: gameData.lpm,
avgAccuracy: gameData.accuracy,
totalGames: 1,
totalTime: gameData.totalTime,
bestGameEntryId: gameEntry.id, // Set the current game entry as the best one initially
},
});

console.log('Score submitted successfully.');
}
2 changes: 2 additions & 0 deletions src/app/api/data/leaderboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NextResponse } from 'next/server';

import { prisma } from '@/lib/prisma';

export const dynamic = 'force-dynamic';

export const GET = async (req: Request) => {
try {
const url = new URL(req.url);
Expand Down
40 changes: 40 additions & 0 deletions src/app/api/data/user/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// src/app/api/data/user.ts
import { NextResponse } from 'next/server';

import { prisma } from '@/lib/prisma';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const name = searchParams.get('name');

if (!name) {
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
}

const fullUser = await prisma.user.findUnique({
where: { name: name },
select: {
id: true,
email: true,
name: true,
role: true,
picture: true,
totalGames: true,
totalTime: true,
joinedAt: true,
},
});

if (!fullUser) {
return NextResponse.json(
{ error: 'Full user data not found' },
{ status: 404 }
);
}

const userStats = await prisma.userStats.findMany({
where: { userId: fullUser.id },
});

return NextResponse.json({ user: fullUser, stats: userStats });
}
Loading

0 comments on commit f5bcc14

Please sign in to comment.