Skip to content

[WPM] feat: add WPM functionality to race results and tracking #780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache"
},
"dependencies": {
"@auth/prisma-adapter": "^1.0.0",
"@auth/prisma-adapter": "^2.8.0",
"@code-racer/wss": "file:../wss",
"@hookform/resolvers": "^3.1.1",
"@next-auth/prisma-adapter": "^1.0.7",
Expand Down Expand Up @@ -51,11 +51,11 @@
"clsx": "^1.2.1",
"cmdk": "^0.2.0",
"lucide-react": "^0.259.0",
"next": "13.4.9",
"next": "^13.5.8",
"next-auth": "^4.22.1",
"next-themes": "^0.2.1",
"nextjs-toploader": "^1.4.2",
"openai": "^3.3.0",
"openai": "^4.89.0",
"postcss": "8.4.25",
"prettier": "2.8.8",
"prettier-plugin-go-template": "0.0.13",
Expand Down Expand Up @@ -89,7 +89,7 @@
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"autoprefixer": "10.4.14",
"cypress": "^12.17.2",
"cypress": "^14.2.0",
"eslint": "8.44.0",
"eslint-config-next": "13.4.9",
"eslint-config-prettier": "^8.8.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER');

-- CreateEnum
CREATE TYPE "AchievementType" AS ENUM ('FIRST_RACE', 'FIRST_SNIPPET', 'FIFTH_RACE');

-- CreateEnum
CREATE TYPE "VoteType" AS ENUM ('UP', 'DOWN');

-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT,
"email" TEXT,
"email_verified" TIMESTAMP(3),
"image" TEXT,
"averageAccuracy" DECIMAL(5,2) NOT NULL DEFAULT 0,
"averageCpm" DECIMAL(6,2) NOT NULL DEFAULT 0,
"average_wpm" DECIMAL(6,2) NOT NULL DEFAULT 0,
"role" "UserRole" NOT NULL DEFAULT 'USER',
"bio" TEXT,
"languagesMap" JSONB,
"topLanguages" TEXT[],

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

-- CreateTable
CREATE TABLE "accounts" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"provider_account_id" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,

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

-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"session_token" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,

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

-- CreateTable
CREATE TABLE "verification_tokens" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);

-- CreateTable
CREATE TABLE "results" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"accuracy" DECIMAL(5,2) NOT NULL,
"cpm" INTEGER NOT NULL,
"words_per_minute" INTEGER NOT NULL DEFAULT 0,
"taken_time" TEXT NOT NULL,
"error_count" INTEGER,
"snippetId" TEXT NOT NULL,

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

-- CreateTable
CREATE TABLE "achievements" (
"user_id" TEXT NOT NULL,
"achievement_type" "AchievementType" NOT NULL,
"unlocked_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "achievements_pkey" PRIMARY KEY ("user_id","achievement_type")
);

-- CreateTable
CREATE TABLE "snippets" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"language" TEXT NOT NULL,
"user_id" TEXT,
"on_review" BOOLEAN NOT NULL DEFAULT false,
"rating" INTEGER NOT NULL DEFAULT 0,
"name" TEXT,

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

-- CreateTable
CREATE TABLE "race" (
"id" TEXT NOT NULL,
"snippet_id" TEXT NOT NULL,
"started_at" TIMESTAMP(3),
"ended_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

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

-- CreateTable
CREATE TABLE "race_participants" (
"id" TEXT NOT NULL,
"raceId" TEXT NOT NULL,
"user_id" TEXT,
"result_id" TEXT,

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

-- CreateTable
CREATE TABLE "snippet_votes" (
"snippetId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" "VoteType" NOT NULL,

CONSTRAINT "snippet_votes_pkey" PRIMARY KEY ("userId","snippetId")
);

-- CreateTable
CREATE TABLE "notification" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"cta_url" TEXT,
"read" BOOLEAN NOT NULL DEFAULT false,
"user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

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

-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");

-- CreateIndex
CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id");

-- CreateIndex
CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token");

-- CreateIndex
CREATE UNIQUE INDEX "verification_tokens_token_key" ON "verification_tokens"("token");

-- CreateIndex
CREATE UNIQUE INDEX "verification_tokens_identifier_token_key" ON "verification_tokens"("identifier", "token");

-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "results" ADD CONSTRAINT "results_snippetId_fkey" FOREIGN KEY ("snippetId") REFERENCES "snippets"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "results" ADD CONSTRAINT "results_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "achievements" ADD CONSTRAINT "achievements_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "snippets" ADD CONSTRAINT "snippets_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "race" ADD CONSTRAINT "race_snippet_id_fkey" FOREIGN KEY ("snippet_id") REFERENCES "snippets"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "race_participants" ADD CONSTRAINT "race_participants_raceId_fkey" FOREIGN KEY ("raceId") REFERENCES "race"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "race_participants" ADD CONSTRAINT "race_participants_result_id_fkey" FOREIGN KEY ("result_id") REFERENCES "results"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "race_participants" ADD CONSTRAINT "race_participants_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "snippet_votes" ADD CONSTRAINT "snippet_votes_snippetId_fkey" FOREIGN KEY ("snippetId") REFERENCES "snippets"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "snippet_votes" ADD CONSTRAINT "snippet_votes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "notification" ADD CONSTRAINT "notification_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
Warnings:

- You are about to drop the column `words_per_minute` on the `results` table. All the data in the column will be lost.
- You are about to drop the column `average_wpm` on the `users` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "results" DROP COLUMN "words_per_minute",
ADD COLUMN "wpm" INTEGER NOT NULL DEFAULT 0;

-- AlterTable
ALTER TABLE "users" DROP COLUMN "average_wpm",
ADD COLUMN "averageWpm" DECIMAL(6,2) NOT NULL DEFAULT 0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
Warnings:

- You are about to drop the column `wpm` on the `results` table. All the data in the column will be lost.
- You are about to drop the column `averageWpm` on the `users` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "results" DROP COLUMN "wpm",
ADD COLUMN "words_per_minute" INTEGER NOT NULL DEFAULT 0;

-- AlterTable
ALTER TABLE "users" DROP COLUMN "averageWpm",
ADD COLUMN "average_wpm" DECIMAL(6,2) NOT NULL DEFAULT 0;
3 changes: 3 additions & 0 deletions packages/app/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 3 additions & 1 deletion packages/app/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ model User {
image String?
averageAccuracy Decimal @default(0) @db.Decimal(5, 2)
averageCpm Decimal @default(0) @db.Decimal(6, 2)
averageWpm Decimal @default(0) @db.Decimal(6, 2) @map("average_wpm")
role UserRole @default(USER)
bio String?
languagesMap Json?
Expand Down Expand Up @@ -80,6 +81,7 @@ model Result {
takenTime String @map("taken_time")
errorCount Int? @map("error_count")
snippetId String
wpm Int @default(0) @map("words_per_minute")
RaceParticipant RaceParticipant[]
snippet Snippet @relation(fields: [snippetId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
Expand All @@ -106,7 +108,7 @@ model Snippet {
rating Int @default(0)
name String?
Race Race[]
Result Result[]
Result Result[]
votes SnippetVote[]
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)

Expand Down
55 changes: 24 additions & 31 deletions packages/app/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ declare module "next-auth" {
export const nextAuthOptions = {
adapter: PrismaAdapter(prisma),
session: {
strategy: "jwt",
strategy: "database",
},
secret: env.NEXTAUTH_SECRET,
providers: [
Expand All @@ -28,46 +28,39 @@ export const nextAuthOptions = {
}),
],
callbacks: {
async signIn(options) {
async signIn({ user }) {
const racerCode = randomstring.generate({
length: 4,
charset: "numeric",
});
options.user.email = `${options.user.id}@example.com`;
options.user.name = `Racer ${racerCode}`;
return true;
},
async jwt({ token, user }) {
const dbUser = await prisma.user.findFirst({
where: {
email: token.email,
},

// Check if user exists
const existingUser = await prisma.user.findUnique({
where: { email: user.email || `${user.id}@example.com` },
});

if (!dbUser) {
if (user) {
token.id = user.id;
}
return token;
if (!existingUser) {
// Create new user if doesn't exist
await prisma.user.create({
data: {
email: user.email || `${user.id}@example.com`,
name: `Racer ${racerCode}`,
averageAccuracy: 0,
averageCpm: 0,
averageWpm: 0,
role: "USER",
image: user.image,
},
});
}

return {
id: dbUser.id,
name: dbUser.name,
email: dbUser.email,
role: dbUser.role,
picture: dbUser.image,
};
return true;
},
async session({ token, session }) {
if (token) {
session.user.id = token.id;
session.user.name = token.name;
session.user.email = token.email;
session.user.role = token.role;
session.user.image = token.picture;
async session({ session, user }) {
if (session.user) {
session.user.id = user.id;
session.user.role = user.role;
}

return session;
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,16 @@ export default function PerformanceComparison({
return (
<Tabs defaultValue="cpm" className="w-full">
<TabsList>
<TabsTrigger value="cpm">Cpm</TabsTrigger>
<TabsTrigger value="cpm">CPM</TabsTrigger>
<TabsTrigger value="wpm">WPM</TabsTrigger>
<TabsTrigger value="accuracy">Accuracy</TabsTrigger>
</TabsList>
<TabsContent value="cpm">
<BubbleChart obj="cpm" usersData={recentGames} />
</TabsContent>
<TabsContent value="wpm">
<BubbleChart obj="wpm" usersData={recentGames} />
</TabsContent>
<TabsContent value="accuracy">
<BubbleChart obj="accuracy" usersData={recentGames} />
</TabsContent>
Expand Down
Loading