diff --git a/package-lock.json b/package-lock.json index 09ace0fd..4e3e0ec6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,7 +106,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -118,7 +118,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -624,7 +624,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.0.0.tgz", "integrity": "sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==", - "devOptional": true, + "dev": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { @@ -1751,25 +1751,25 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true + "dev": true }, "node_modules/@types/body-parser": { "version": "1.19.2", @@ -1933,7 +1933,7 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "devOptional": true + "dev": true }, "node_modules/@types/qs": { "version": "6.9.7", @@ -1957,7 +1957,7 @@ "version": "18.2.16", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.16.tgz", "integrity": "sha512-LLFWr12ZhBJ4YVw7neWLe6Pk7Ey5R9OCydfuMsz1L8bZxzaawJj2p06Q8/EFEHDeTBQNFLF62X+CG7B2zIyu0Q==", - "devOptional": true, + "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1968,7 +1968,7 @@ "version": "18.2.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", "integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==", - "devOptional": true, + "dev": true, "dependencies": { "@types/react": "*" } @@ -1977,7 +1977,7 @@ "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "devOptional": true + "dev": true }, "node_modules/@types/semver": { "version": "7.5.0", @@ -2249,7 +2249,7 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "devOptional": true, + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2270,7 +2270,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.4.0" } @@ -3588,7 +3588,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true + "dev": true }, "node_modules/cross-env": { "version": "7.0.3", @@ -6905,7 +6905,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true + "dev": true }, "node_modules/map-stream": { "version": "0.1.0", @@ -8054,7 +8054,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.0.0.tgz", "integrity": "sha512-KYWk83Fhi1FH59jSpavAYTt2eoMVW9YKgu8ci0kuUnt6Dup5Qy47pcB4/TLmiPAbhGrxxSz7gsSnJcCmkyPANA==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "5.0.0" @@ -9818,7 +9818,7 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "devOptional": true, + "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9861,13 +9861,13 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true + "dev": true }, "node_modules/ts-node/node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.3.1" } @@ -10037,6 +10037,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10219,7 +10220,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true + "dev": true }, "node_modules/vary": { "version": "1.1.2", @@ -10478,7 +10479,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6" } @@ -10630,6 +10631,7 @@ "@types/node": "^20.4.5", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", + "dotenv": "^16.3.1", "eslint": "^8.45.0", "nodemon": "^3.0.1", "prettier": "^3.0.0" diff --git a/package.json b/package.json index b7cf00b1..ef36739e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "dev:db": "npm run db --workspace @code-racer/app", "dev:wss": "cross-env PORT=3001 npm run dev --workspace @code-racer/wss", "deploy:wss": "source packages/wss/.env && fly deploy --build-arg DATABASE_URL=$DATABASE_URL --config packages/wss/fly.toml --dockerfile packages/wss/Dockerfile", - "pr:precheck": "npm run lint -w @code-racer/app && npm run type-check -w @code-racer/app && npm run build -w @code-racer/app" + "pr:precheck": "npm run lint -w @code-racer/app && npm run type-check -w @code-racer/app && npm run build -w @code-racer/app", + "format:write": "echo 'Formatting app' && npm run format:write --workspace @code-racer/app && echo 'Formatting server...' && npm run format:write --workspace @code-racer/wss", + "e2e:app": "npm run e2e --workspace @code-racer/app" }, "repository": { "type": "git", diff --git a/packages/app/.eslintrc.json b/packages/app/.eslintrc.json index d2ca893f..688cf918 100644 --- a/packages/app/.eslintrc.json +++ b/packages/app/.eslintrc.json @@ -15,7 +15,7 @@ "quotes": ["error", "double", { "allowTemplateLiterals": true }], "react/react-in-jsx-scope": "off", "jsx-a11y/anchor-is-valid": "off", - "no-unused-vars": "warn", + "no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], "@typescript-eslint/no-empty-interface": "warn", "@typescript-eslint/no-explicit-any": "warn", diff --git a/packages/app/cypress.config.ts b/packages/app/cypress.config.ts index 3e839631..2a7f668c 100644 --- a/packages/app/cypress.config.ts +++ b/packages/app/cypress.config.ts @@ -10,4 +10,11 @@ export default defineConfig({ baseUrl: "http://localhost:3000", scrollBehavior: "center", }, + + component: { + devServer: { + framework: "next", + bundler: "webpack", + }, + }, }); diff --git a/packages/app/cypress/component/TestComponentName.cy.tsx b/packages/app/cypress/component/TestComponentName.cy.tsx index c608253d..981049c7 100644 --- a/packages/app/cypress/component/TestComponentName.cy.tsx +++ b/packages/app/cypress/component/TestComponentName.cy.tsx @@ -1,5 +1,5 @@ describe("ComponentName.cy.tsx", () => { it("playground", () => { // cy.mount() - }) -}) \ No newline at end of file + }); +}); diff --git a/packages/app/cypress/e2e/business-layer/raceBL.ts b/packages/app/cypress/e2e/business-layer/raceBL.ts index d4b20b15..3d87eaf5 100644 --- a/packages/app/cypress/e2e/business-layer/raceBL.ts +++ b/packages/app/cypress/e2e/business-layer/raceBL.ts @@ -1,35 +1,34 @@ import { RacePage } from "../page-objects/pages/RacePage"; export class RaceBL { - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public static runTypingRace(spans: any): RacePage{ - const NEW_LINE = "⏎\n"; - let code = ""; - let isIndentWhiteSpace = false; - for (let i = 0; i < spans.length; i++) { - const char = spans[i].innerText; - if (char !== " " && isIndentWhiteSpace) { - // Encounter non-whitespace character when isIndentWhiteSpace=true - // Unset isIndentWhiteSpace back to false - isIndentWhiteSpace = false; - } - if (char === " " && isIndentWhiteSpace) { - continue; - } - code += char === NEW_LINE ? "\n" : char; - if (char === NEW_LINE) { - // When we encounter new line, the following whitespace up to - // encounting non-whitespace character will be considered indent whitespace. - // Since our app auto indent, we don't type it - isIndentWhiteSpace = true; - } - } - cy.get("[data-cy=\"race-practice-input\"]").type(code, { - force: true, - parseSpecialCharSequences: false, - delay: 30, - waitForAnimations: true, - }); - return new RacePage(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public static runTypingRace(spans: Array): RacePage { + const NEW_LINE = "⏎\n"; + let code = ""; + let isIndentWhiteSpace = false; + for (let i = 0; i < spans.length; ++i) { + const char = spans[i].innerText; + if (char !== " " && isIndentWhiteSpace) { + // Encounter non-whitespace character when isIndentWhiteSpace=true + // Unset isIndentWhiteSpace back to false + isIndentWhiteSpace = false; + } + if (char === " " && isIndentWhiteSpace) { + continue; + } + code += char === NEW_LINE ? "\n" : char; + if (char === NEW_LINE) { + // When we encounter new line, the following whitespace up to + // encounting non-whitespace character will be considered indent whitespace. + // Since our app auto indent, we don't type it + isIndentWhiteSpace = true; + } } -} \ No newline at end of file + cy.get("[data-cy='race-practice-input']").type(code, { + force: true, + parseSpecialCharSequences: false, + delay: 30, + waitForAnimations: true, + }); + return new RacePage(); + } +} diff --git a/packages/app/cypress/e2e/page-objects/components/NavbarComponent.ts b/packages/app/cypress/e2e/page-objects/components/NavbarComponent.ts index e2564473..77e7bd66 100644 --- a/packages/app/cypress/e2e/page-objects/components/NavbarComponent.ts +++ b/packages/app/cypress/e2e/page-objects/components/NavbarComponent.ts @@ -1,14 +1,9 @@ -import { siteConfig } from "../../../../src/config/site"; -const headerLinks = siteConfig.getHeaderLinks(false); export class NavbarComponent { + race(): Cypress.Chainable { + return cy.get("[data-cy='Race-main-nav-link']"); + } - race(): Cypress.Chainable{ return cy.get(`[data-cy="${ - headerLinks.find((e) => e.title.includes("Race"))?.title ?? "Race" - }-main-nav-link"]`, - )} - - leaderboard(): Cypress.Chainable{ return cy.get(`[data-cy="${ - headerLinks.find((e) => e.title.includes("Leaderboard"))?.title ?? "Leaderboard" - }-main-nav-link"]`, - )} -} \ No newline at end of file + leaderboard(): Cypress.Chainable { + return cy.get("[data-cy='Leaderboard-main-nav-link']"); + } +} diff --git a/packages/app/cypress/e2e/page-objects/pages/ContributorsPage.ts b/packages/app/cypress/e2e/page-objects/pages/ContributorsPage.ts index da2eabf7..2d599485 100644 --- a/packages/app/cypress/e2e/page-objects/pages/ContributorsPage.ts +++ b/packages/app/cypress/e2e/page-objects/pages/ContributorsPage.ts @@ -1,11 +1,20 @@ -export class ContributorsPage{ +export class ContributorsPage { + contributorCard(): Cypress.Chainable { + return cy.get('[data-cy="contributor-card"]'); + } + contributorName(): Cypress.Chainable { + return cy.get('[data-cy="contributor-name"]'); + } + commitDisplay(): Cypress.Chainable { + return cy.get('[data-cy="github-commit-display"]'); + } + commitLink(): Cypress.Chainable { + return cy.get('[data-cy="github-commit-link"]'); + } - contributorCard(): Cypress.Chainable{ return cy.get("[data-cy=\"contributor-card\"]") } - contributorName(): Cypress.Chainable{ return cy.get("[data-cy=\"contributor-name\"]") } - commitDisplay(): Cypress.Chainable{ return cy.get("[data-cy=\"github-commit-display\"]") } - commitLink(): Cypress.Chainable{ return cy.get("[data-cy=\"github-commit-link\"]") } - - public contributorCardText(contributorCard: Cypress.Chainable): Cypress.Chainable{ - return contributorCard.find("[data-cy=\"contributor-name\"]").invoke("text") - } -} \ No newline at end of file + public contributorCardText( + contributorCard: Cypress.Chainable + ): Cypress.Chainable { + return contributorCard.find('[data-cy="contributor-name"]').invoke("text"); + } +} diff --git a/packages/app/cypress/e2e/page-objects/pages/LeaderboardPage.ts b/packages/app/cypress/e2e/page-objects/pages/LeaderboardPage.ts index 4b17c902..4ce7fc8c 100644 --- a/packages/app/cypress/e2e/page-objects/pages/LeaderboardPage.ts +++ b/packages/app/cypress/e2e/page-objects/pages/LeaderboardPage.ts @@ -1,8 +1,15 @@ -export class LeaderboardPage{ +export class LeaderboardPage { + userRow(): Cypress.Chainable { + return cy.get(".table-row"); + } + user(): Cypress.Chainable { + return cy.get(".flex.items-center.gap-2"); + } + rowDropdown(): Cypress.Chainable { + return cy.get('[role="combobox"]'); + } - userRow(): Cypress.Chainable{ return cy.get(".table-row") } - user(): Cypress.Chainable{ return cy.get(".flex.items-center.gap-2") } - rowDropdown(): Cypress.Chainable{ return cy.get("[role=\"combobox\"]") } - - rowDropdownElement(): Cypress.Chainable{ return cy.get("[role=\"option\"]") } -} \ No newline at end of file + rowDropdownElement(): Cypress.Chainable { + return cy.get('[role="option"]'); + } +} diff --git a/packages/app/cypress/e2e/page-objects/pages/RacePage.ts b/packages/app/cypress/e2e/page-objects/pages/RacePage.ts index b4b93437..5f823be6 100644 --- a/packages/app/cypress/e2e/page-objects/pages/RacePage.ts +++ b/packages/app/cypress/e2e/page-objects/pages/RacePage.ts @@ -1,6 +1,8 @@ export class RacePage { practiceCardLanguageDropdown(): Cypress.Chainable { - return cy.get("[data-cy='practice-card'] [data-cy='language-dropdown']"); + return cy.get( + "[data-cy='practice-card'] [data-cy='language-dropdown'] [data-cy='search-language-input']" + ); } cppLanguageOption(): Cypress.Chainable { return cy.get("[data-cy='c++-value']"); diff --git a/packages/app/cypress/e2e/page-objects/pages/index.ts b/packages/app/cypress/e2e/page-objects/pages/index.ts index 6764d904..77a2fc59 100644 --- a/packages/app/cypress/e2e/page-objects/pages/index.ts +++ b/packages/app/cypress/e2e/page-objects/pages/index.ts @@ -1,3 +1,3 @@ -export * from "./ContributorsPage" -export * from "./LeaderboardPage" -export * from "./RacePage" +export * from "./ContributorsPage"; +export * from "./LeaderboardPage"; +export * from "./RacePage"; diff --git a/packages/app/cypress/e2e/tests/TestPracticeRace.cy.js b/packages/app/cypress/e2e/tests/TestPracticeRace.cy.js index ecd7b645..239a1bcb 100644 --- a/packages/app/cypress/e2e/tests/TestPracticeRace.cy.js +++ b/packages/app/cypress/e2e/tests/TestPracticeRace.cy.js @@ -13,9 +13,9 @@ beforeEach(() => { cy.visit("/"); }); -const TIME_TO_WAIT = 1000; +const TIME_TO_WAIT = 10000; -it("can successfully completed a practice race", () => { +it("Can successfully finish a race lifecycle in practice mode", () => { // Find Race Navigation and click on it navbarComponent.race().should("be.visible").click(); diff --git a/packages/app/cypress/support/component-index.html b/packages/app/cypress/support/component-index.html new file mode 100644 index 00000000..3e16e9b0 --- /dev/null +++ b/packages/app/cypress/support/component-index.html @@ -0,0 +1,14 @@ + + + + + + + Components App + +
+ + +
+ + \ No newline at end of file diff --git a/packages/app/cypress/support/component.ts b/packages/app/cypress/support/component.ts new file mode 100644 index 00000000..37f59edb --- /dev/null +++ b/packages/app/cypress/support/component.ts @@ -0,0 +1,39 @@ +// *********************************************************** +// This example support/component.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +import { mount } from 'cypress/react18' + +// Augment the Cypress namespace to include type definitions for +// your custom command. +// Alternatively, can be defined in cypress/support/component.d.ts +// with a at the top of your spec. +declare global { + namespace Cypress { + interface Chainable { + mount: typeof mount + } + } +} + +Cypress.Commands.add('mount', mount) + +// Example use: +// cy.mount() \ No newline at end of file diff --git a/packages/app/prisma/seed-data/users.seed.ts b/packages/app/prisma/seed-data/users.seed.ts index 64726ac3..80e963c2 100644 --- a/packages/app/prisma/seed-data/users.seed.ts +++ b/packages/app/prisma/seed-data/users.seed.ts @@ -1,56 +1,56 @@ import { Prisma, type User } from "@prisma/client"; function generateRandomCPM() { - return new Prisma.Decimal(Math.floor(Math.random() * (150 - 80) + 80)); + return new Prisma.Decimal(Math.floor(Math.random() * (150 - 80) + 80)); } function generateRandomAccuracy() { - return new Prisma.Decimal(parseFloat((Math.random() * 30 + 70).toFixed(2))); + return new Prisma.Decimal(parseFloat((Math.random() * 30 + 70).toFixed(2))); } export const users: { - id: User["id"]; - name: User["name"]; - averageCpm: User["averageCpm"]; - averageAccuracy: User["averageAccuracy"]; - // Racesplayed, this would required more seeds to be made avoiding for now - topLanguages: User["topLanguages"]; + id: User["id"]; + name: User["name"]; + averageCpm: User["averageCpm"]; + averageAccuracy: User["averageAccuracy"]; + // Racesplayed, this would required more seeds to be made avoiding for now + topLanguages: User["topLanguages"]; }[] = [ - { - id: "clktzbpun000008jv0wnqgwxz", - name: "Cody", - averageCpm: generateRandomCPM(), - averageAccuracy: generateRandomAccuracy(), - topLanguages: ["typescript", "go", "c#"] - }, - { - id: "clktzpq7m000008jv3l0u7a88", - name: "Chris", - averageCpm: generateRandomCPM(), - averageAccuracy: generateRandomAccuracy(), - topLanguages: ["typescript", "python"] - }, - { - id: "clktzpunl000108jvhyza3x0z", - name: "Jordan", - averageCpm: generateRandomCPM(), - averageAccuracy: generateRandomAccuracy(), - topLanguages: ["typescript", "go", "html"] - }, - { - id: "clktzpyd3000208jv99s31kgm", - name: "Bob", - averageCpm: generateRandomCPM(), - averageAccuracy: generateRandomAccuracy(), - topLanguages: ["html", "java"] - }, - { - id: "clktzq13s000308jv71zwbht0", - name: "Alice", - averageCpm: generateRandomCPM(), - averageAccuracy: generateRandomAccuracy(), - topLanguages: ["php", "go"] - } - ] + { + id: "clktzbpun000008jv0wnqgwxz", + name: "Cody", + averageCpm: generateRandomCPM(), + averageAccuracy: generateRandomAccuracy(), + topLanguages: ["typescript", "go", "c#"], + }, + { + id: "clktzpq7m000008jv3l0u7a88", + name: "Chris", + averageCpm: generateRandomCPM(), + averageAccuracy: generateRandomAccuracy(), + topLanguages: ["typescript", "python"], + }, + { + id: "clktzpunl000108jvhyza3x0z", + name: "Jordan", + averageCpm: generateRandomCPM(), + averageAccuracy: generateRandomAccuracy(), + topLanguages: ["typescript", "go", "html"], + }, + { + id: "clktzpyd3000208jv99s31kgm", + name: "Bob", + averageCpm: generateRandomCPM(), + averageAccuracy: generateRandomAccuracy(), + topLanguages: ["html", "java"], + }, + { + id: "clktzq13s000308jv71zwbht0", + name: "Alice", + averageCpm: generateRandomCPM(), + averageAccuracy: generateRandomAccuracy(), + topLanguages: ["php", "go"], + }, +]; -export default users; \ No newline at end of file +export default users; diff --git a/packages/app/prisma/seed.ts b/packages/app/prisma/seed.ts index 44acb63d..a05e00d1 100644 --- a/packages/app/prisma/seed.ts +++ b/packages/app/prisma/seed.ts @@ -50,18 +50,21 @@ async function main() { await prisma.user.upsert({ where: { id: user.id }, create: user, - update: { averageAccuracy: user.averageAccuracy, averageCpm: user.averageCpm } + update: { + averageAccuracy: user.averageAccuracy, + averageCpm: user.averageCpm, + }, }); await prisma.achievement.upsert({ where: { userId_achievementType: { userId: user.id, - achievementType: "FIFTH_RACE" - } + achievementType: "FIFTH_RACE", + }, }, create: { userId: user.id, achievementType: "FIFTH_RACE" }, - update: {} + update: {}, }); } } diff --git a/packages/app/src/app/add-snippet/_components/add-snippet-form.tsx b/packages/app/src/app/add-snippet/_components/add-snippet-form.tsx index 218818b5..01663012 100644 --- a/packages/app/src/app/add-snippet/_components/add-snippet-form.tsx +++ b/packages/app/src/app/add-snippet/_components/add-snippet-form.tsx @@ -20,13 +20,10 @@ import { addSnippetAction, addSnippetForReviewAction } from "./actions"; import LanguageDropDown from "./language-dropdown"; import { catchError } from "@/lib/utils"; import { unlockAchievement } from "@/components/achievement"; +import { languageTypes } from "@/lib/validations/room"; const formDataSchema = z.object({ - codeLanguage: z - .string({ - required_error: "Please select a language", - }) - .nonempty(), + codeLanguage: languageTypes, codeSnippet: z .string({ required_error: "Please enter a code snippet", @@ -41,7 +38,11 @@ const formDataSchema = z.object({ type FormData = z.infer; -export default function AddSnippetForm({ lang }: { lang: string }) { +export default function AddSnippetForm({ + lang, +}: { + lang: z.infer; +}) { const { toast, dismiss } = useToast(); const form = useForm({ @@ -111,7 +112,7 @@ export default function AddSnippetForm({ lang }: { lang: string }) { responseData?.message === "snippet-created-and-achievement-unlocked" ) { const firstSnippetAchievement = achievements.find( - (achievement) => achievement.type === "FIRST_SNIPPET", + (achievement) => achievement.type === "FIRST_SNIPPET" ); if (firstSnippetAchievement) unlockAchievement({ diff --git a/packages/app/src/app/add-snippet/_components/language-dropdown.tsx b/packages/app/src/app/add-snippet/_components/language-dropdown.tsx index b09432f1..d5e8ec89 100644 --- a/packages/app/src/app/add-snippet/_components/language-dropdown.tsx +++ b/packages/app/src/app/add-snippet/_components/language-dropdown.tsx @@ -1,9 +1,10 @@ "use client"; -import * as React from "react"; +import React from "react"; +import { z } from "zod"; + import { Check, ChevronsUpDown } from "lucide-react"; -import { useEffect } from "react"; -import { cn } from "@/lib/utils"; + import { Button } from "@/components/ui/button"; import { Command, @@ -17,28 +18,51 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; + import { snippetLanguages } from "@/config/languages"; +import { type LanguageType, languageTypes } from "@/lib/validations/room"; +import { cn } from "@/lib/utils"; + +type LanguageTypes = z.infer; + const LanguageDropdown = ({ className, value, onChange, }: { className?: string; - value: string; - // eslint-disable-next-line no-unused-vars - onChange: (props: React.SetStateAction) => void; + value: LanguageTypes | undefined; + onChange: (_props: React.SetStateAction) => void; }) => { const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(""); - useEffect(() => { - const savedCodeLanguage = window.localStorage.getItem("codeLanguage"); - if (savedCodeLanguage) { - onChange(savedCodeLanguage); + const itemToShow = React.useMemo(() => { + if (value) { + for (let idx = 0; idx < snippetLanguages.length; ++idx) { + if (snippetLanguages[idx].value === value) { + return snippetLanguages[idx].label; + } + } + } else { + return "Select language..."; + } + }, [value]); + + React.useEffect(() => { + if (localStorage) { + const savedCodeLanguage = localStorage.getItem("codeLanguage"); + const isLanguageValid = + languageTypes.safeParse(savedCodeLanguage).success; + if (isLanguageValid) { + onChange(savedCodeLanguage as LanguageType); + } else { + onChange("c++"); + } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [onChange]); + return ( @@ -49,10 +73,7 @@ const LanguageDropdown = ({ className={cn("justify-between w-full px-4 py-3", className)} data-cy="language-dropdown" > - {value - ? snippetLanguages.find((language) => language.value === value) - ?.label - : "Select language..."} + {itemToShow} @@ -68,29 +89,24 @@ const LanguageDropdown = ({ {snippetLanguages .filter((language) => - language.label.toLowerCase().includes(search.toLowerCase()), + language.label.toLowerCase().includes(search.toLowerCase()) ) .map((language) => ( { - const newCodeLanguage = - currentValue === value ? "" : currentValue; - onChange(newCodeLanguage); - window.localStorage.setItem( - "codeLanguage", - newCodeLanguage, - ); + const parsedValue = languageTypes.parse(currentValue); + onChange(parsedValue); + window.localStorage.setItem("codeLanguage", parsedValue); setOpen(false); }} data-cy={`${language.value}-value`} > {language.label} @@ -104,6 +120,4 @@ const LanguageDropdown = ({ LanguageDropdown.displayName = "LanguageDropdown"; -LanguageDropdown.displayName = "LanguageDropdown"; - -export default LanguageDropdown; +export default React.memo(LanguageDropdown); diff --git a/packages/app/src/app/add-snippet/page.tsx b/packages/app/src/app/add-snippet/page.tsx index 79c79474..9b2d60ab 100644 --- a/packages/app/src/app/add-snippet/page.tsx +++ b/packages/app/src/app/add-snippet/page.tsx @@ -1,12 +1,14 @@ "use client"; import { Heading } from "@/components/ui/heading"; import AddSnippetForm from "./_components/add-snippet-form"; +import { z } from "zod"; +import { languageTypes } from "@/lib/validations/room"; export default function AddSnippet({ searchParams, }: { searchParams: { - lang: string; + lang: z.infer; }; }) { const language = searchParams.lang; diff --git a/packages/app/src/app/api/random/route.ts b/packages/app/src/app/api/random/route.ts index 390745ef..99dce7e7 100644 --- a/packages/app/src/app/api/random/route.ts +++ b/packages/app/src/app/api/random/route.ts @@ -1,7 +1,7 @@ -import { NextResponse } from "next/server" - -export const dynamic = "force-dynamic" +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; export async function GET() { - return NextResponse.json({ data: Math.random() }) -} \ No newline at end of file + return NextResponse.json({ data: Math.random() }); +} diff --git a/packages/app/src/app/contributors/_components/additions-deletions.tsx b/packages/app/src/app/contributors/_components/additions-deletions.tsx index c5dc6b93..3aeac0e9 100644 --- a/packages/app/src/app/contributors/_components/additions-deletions.tsx +++ b/packages/app/src/app/contributors/_components/additions-deletions.tsx @@ -23,7 +23,7 @@ export default function AdditionsDeletions({ "p-0", "font-md", "text-muted-foreground", - className, + className )} > diff --git a/packages/app/src/app/contributors/_components/counting-animation.tsx b/packages/app/src/app/contributors/_components/counting-animation.tsx index 49b5de56..d4229909 100644 --- a/packages/app/src/app/contributors/_components/counting-animation.tsx +++ b/packages/app/src/app/contributors/_components/counting-animation.tsx @@ -24,7 +24,7 @@ export default function CountingAnimation({ // Amount to increment in every update cycle const incrementValue = Math.ceil( - (targetNumber - startingNumber) / (duration / UPDATE_INTERVAL), + (targetNumber - startingNumber) / (duration / UPDATE_INTERVAL) ); function update() { diff --git a/packages/app/src/app/contributors/_components/pagination-bar.tsx b/packages/app/src/app/contributors/_components/pagination-bar.tsx index 921b3e7b..a7002053 100644 --- a/packages/app/src/app/contributors/_components/pagination-bar.tsx +++ b/packages/app/src/app/contributors/_components/pagination-bar.tsx @@ -23,7 +23,7 @@ export default function PaginationBar({ totalPages, }: PaginationBarProps) { return ( -
+
{pages === 1 ? (
@@ -39,13 +39,23 @@ export default function PaginationBar({ ) : (
- - @@ -80,10 +90,10 @@ export default function PaginationBar({
)} -
-

page {pages} out of {totalPages}

+

+ page {pages} out of {totalPages} +

- ); } diff --git a/packages/app/src/app/contributors/_helpers/utils.ts b/packages/app/src/app/contributors/_helpers/utils.ts index e359d31f..6f3311a9 100644 --- a/packages/app/src/app/contributors/_helpers/utils.ts +++ b/packages/app/src/app/contributors/_helpers/utils.ts @@ -22,7 +22,7 @@ export function displayNumber(number: number): string { } async function getContributorCodeChanges( - contributors: GitHubUser[], + contributors: GitHubUser[] ): Promise { const url = siteConfig.api.github.githubContributorActivity; const commitActivity: ContributorCodeChanges[] = []; @@ -47,13 +47,13 @@ async function getContributorCodeChanges( const activityAllTime = activity.weeks.reduce( ( accumulator: ContributorCodeChanges, - currentValue: { a: number; d: number; w: number; c: number }, + currentValue: { a: number; d: number; w: number; c: number } ) => ({ ...accumulator, additions: currentValue.a + accumulator.additions, deletions: currentValue.d + accumulator.deletions, }), - { additions: 0, deletions: 0, login: username }, + { additions: 0, deletions: 0, login: username } ); commitActivity.push(activityAllTime); } @@ -117,7 +117,7 @@ async function getRepoWeeklyCodeChanges(): Promise< async function getContributorCommitList( contributor: GitHubUser, - take = 5, + take = 5 ): Promise { const searchParams = new URLSearchParams({ author: contributor.login, diff --git a/packages/app/src/app/contributors/page.tsx b/packages/app/src/app/contributors/page.tsx index 1bc4002b..a098ea2e 100644 --- a/packages/app/src/app/contributors/page.tsx +++ b/packages/app/src/app/contributors/page.tsx @@ -48,7 +48,7 @@ export default async function ContributorsPage({ const contributors = await getContributors(); const totalPage = Math.ceil(contributors.length / per_page); const contributorCommitActivities = await getContributorCodeChanges( - contributors, + contributors ); const repoCommitActivity = await getRepoWeeklyCodeChanges(); const [since, additions, deletions] = @@ -94,11 +94,11 @@ export default async function ContributorsPage({ className="mt-3" nextURL={`/contributors?page=${Math.min( page + 1, - totalPage, + totalPage )}&per_page=${per_page}`} prevURL={`/contributors?page=${Math.max( page - 1, - 1, + 1 )}&per_page=${per_page}`} firstURL={`/contributors?page=1&per_page=${per_page}`} lastURL={`/contributors?page=${totalPage}&per_page=${per_page}`} @@ -116,7 +116,7 @@ export default async function ContributorsPage({ contributor={contributor} contributorsCodeChanges={ contributorCommitActivities.find( - (e) => e.login === contributor.login, + (e) => e.login === contributor.login ) ?? { additions: 0, deletions: 0, login: contributor.login } } /> @@ -127,11 +127,11 @@ export default async function ContributorsPage({ className="flex justify-center w-full mt-6" nextURL={`/contributors?page=${Math.min( page + 1, - totalPage, + totalPage )}&per_page=${per_page}`} prevURL={`/contributors?page=${Math.max( page - 1, - 1, + 1 )}&per_page=${per_page}`} firstURL={`/contributors?page=1&per_page=${per_page}`} lastURL={`/contributors?page=${totalPage}&per_page=${per_page}`} diff --git a/packages/app/src/app/dashboard/_components/performanceComparison.tsx b/packages/app/src/app/dashboard/_components/performanceComparison.tsx index e44de214..0dbb5280 100644 --- a/packages/app/src/app/dashboard/_components/performanceComparison.tsx +++ b/packages/app/src/app/dashboard/_components/performanceComparison.tsx @@ -24,14 +24,14 @@ const parseDomain = (usersData: Result[], obj: ObjectKey) => [ 0, Math.max( ...usersData.map((value) => - obj === "cpm" ? value.cpm : Number(value.accuracy), - ), + obj === "cpm" ? value.cpm : Number(value.accuracy) + ) ), ]; const renderTooltip = ( props: TooltipProps, - obj: ObjectKey, + obj: ObjectKey ) => { const { active, payload } = props; @@ -108,7 +108,7 @@ function BubbleChart({ ) : ( - ), + ) )} diff --git a/packages/app/src/app/dashboard/_components/raceTableServerSide.tsx b/packages/app/src/app/dashboard/_components/raceTableServerSide.tsx index 1b2b210e..21dbba78 100644 --- a/packages/app/src/app/dashboard/_components/raceTableServerSide.tsx +++ b/packages/app/src/app/dashboard/_components/raceTableServerSide.tsx @@ -25,7 +25,7 @@ export default async function RaceTableServerSide({ typeof sort === "string" ? (sort.split(".") as [ keyof Result | undefined, - "asc" | "desc" | undefined, + "asc" | "desc" | undefined ]) : []; diff --git a/packages/app/src/app/dashboard/_components/snippetHist.tsx b/packages/app/src/app/dashboard/_components/snippetHist.tsx index 0971180a..48b384df 100644 --- a/packages/app/src/app/dashboard/_components/snippetHist.tsx +++ b/packages/app/src/app/dashboard/_components/snippetHist.tsx @@ -46,7 +46,7 @@ function getCustomisedData(data: Snippet[]) { language, count, }; - }, + } ); return dataCustomised; diff --git a/packages/app/src/app/dashboard/_components/snippetsTable.tsx b/packages/app/src/app/dashboard/_components/snippetsTable.tsx index 34a198c5..435ace0c 100644 --- a/packages/app/src/app/dashboard/_components/snippetsTable.tsx +++ b/packages/app/src/app/dashboard/_components/snippetsTable.tsx @@ -37,7 +37,7 @@ export function SnippetsTable({ onCheckedChange={(value) => { table.toggleAllPageRowsSelected(!!value); setSelectedRowIds((prev) => - prev.length === data.length ? [] : data.map((row) => row.id), + prev.length === data.length ? [] : data.map((row) => row.id) ); }} aria-label="Select all" @@ -52,7 +52,7 @@ export function SnippetsTable({ setSelectedRowIds((prev) => value ? [...prev, row.original.id] - : prev.filter((id) => id !== row.original.id), + : prev.filter((id) => id !== row.original.id) ); }} aria-label="Select row" @@ -115,15 +115,15 @@ export function SnippetsTable({ cell: ({ cell }) => { const language = cell.getValue() as Snippet["language"]; const findLanguageLabel = snippetLanguages.find((snippetLanguage) => { - if (snippetLanguage.value === language) { - return snippetLanguage - } - }) + if (snippetLanguage.value === language) { + return snippetLanguage; + } + }); return findLanguageLabel?.label; }, }, ], - [data], + [data] ); async function deleteSelectedRows() { @@ -133,8 +133,8 @@ export function SnippetsTable({ deleteSnippetAction({ id, path: "/dashboard/snippets", - }), - ), + }) + ) ); } catch (err) { catchError(err); diff --git a/packages/app/src/app/dashboard/awards/loaders.ts b/packages/app/src/app/dashboard/awards/loaders.ts index 86c63988..0b67a4ec 100644 --- a/packages/app/src/app/dashboard/awards/loaders.ts +++ b/packages/app/src/app/dashboard/awards/loaders.ts @@ -50,4 +50,4 @@ export async function getUserRank() { const userRank = usersSortedByAccuracy.findIndex((u) => u.id === user.id) + 1; return userRank; -} \ No newline at end of file +} diff --git a/packages/app/src/app/dashboard/awards/page.tsx b/packages/app/src/app/dashboard/awards/page.tsx index 5e206bea..7715b097 100644 --- a/packages/app/src/app/dashboard/awards/page.tsx +++ b/packages/app/src/app/dashboard/awards/page.tsx @@ -5,7 +5,11 @@ import { getCurrentUser } from "@/lib/session"; import { redirect } from "next/navigation"; import { Heading } from "@/components/ui/heading"; import Shell from "@/components/shell"; -import { getUserRank, getUserResultsCount, getUserSnippetCount } from "./loaders"; +import { + getUserRank, + getUserResultsCount, + getUserSnippetCount, +} from "./loaders"; import AchievementProgress from "../_components/achievementProgress"; import { prisma } from "@/lib/prisma"; import { achievements } from "@/config/achievements"; @@ -58,7 +62,10 @@ export default async function AwardsPage({}) { value={totalUserSnippets} />
- + ); } diff --git a/packages/app/src/app/dashboard/races/page.tsx b/packages/app/src/app/dashboard/races/page.tsx index c4424612..8502fed4 100644 --- a/packages/app/src/app/dashboard/races/page.tsx +++ b/packages/app/src/app/dashboard/races/page.tsx @@ -31,7 +31,7 @@ export default async function RacesPage({ typeof sort === "string" ? (sort.split(".") as [ keyof Result | undefined, - "asc" | "desc" | undefined, + "asc" | "desc" | undefined ]) : []; diff --git a/packages/app/src/app/dashboard/snippets/page.tsx b/packages/app/src/app/dashboard/snippets/page.tsx index 20216e9e..b37f6973 100644 --- a/packages/app/src/app/dashboard/snippets/page.tsx +++ b/packages/app/src/app/dashboard/snippets/page.tsx @@ -31,7 +31,7 @@ export default async function SnippetsPage({ typeof sort === "string" ? (sort.split(".") as [ keyof Snippet | undefined, - "asc" | "desc" | undefined, + "asc" | "desc" | undefined ]) : []; diff --git a/packages/app/src/app/hero-banner.tsx b/packages/app/src/app/hero-banner.tsx index 4c47563f..603e4f75 100644 --- a/packages/app/src/app/hero-banner.tsx +++ b/packages/app/src/app/hero-banner.tsx @@ -13,11 +13,8 @@ export default function HeroBanner() {

Test your typing speed and race against other coders

-
+
diff --git a/packages/app/src/app/leaderboard/user-rankings.tsx b/packages/app/src/app/leaderboard/user-rankings.tsx index 6f77c795..ff115572 100644 --- a/packages/app/src/app/leaderboard/user-rankings.tsx +++ b/packages/app/src/app/leaderboard/user-rankings.tsx @@ -1,6 +1,5 @@ import { sortFilters } from "./sort-filters"; - function convertNumberToOrdinal({ n }: { n: number }) { // special case for 11, 12, 13 if (n % 100 === 11 || n % 100 === 12 || n % 100 === 13) { diff --git a/packages/app/src/app/race/(play)/loaders.ts b/packages/app/src/app/race/(play)/loaders.ts index ab4a9c35..bde1080b 100644 --- a/packages/app/src/app/race/(play)/loaders.ts +++ b/packages/app/src/app/race/(play)/loaders.ts @@ -4,6 +4,13 @@ import { type Language } from "../../../config/languages"; import { prisma } from "../../../lib/prisma"; import type { Snippet } from "@prisma/client"; +/** TODO: + * --- + * If we are generating a new random snippet, + * for example, from a practice race page, we + * need to not include the id of the currently + * displayed snippet to get a new one. + */ export async function getRandomSnippet(input: { language: Language; reportedSnippets?: string[]; @@ -40,7 +47,7 @@ export async function getRandomSnippet(input: { } export async function getSnippetById( - snippetId: string, + snippetId: string ): Promise { return await prisma.snippet.findUnique({ where: { diff --git a/packages/app/src/app/race/(play)/multiplayer/page.tsx b/packages/app/src/app/race/(play)/multiplayer/page.tsx deleted file mode 100644 index 9f562f2b..00000000 --- a/packages/app/src/app/race/(play)/multiplayer/page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { getCurrentUser } from "@/lib/session"; -import { redirect } from "next/navigation"; - -import RaceMultiplayer from "../../_components/race/game-multiplayer"; -import { Language, isValidLanguage } from "@/config/languages"; -import RaceMultiplayerRoom from "./room"; - -export default async function MultiplayerRacePage({ - searchParams, -}: { - searchParams: { - lang: string; - }; -}) { - if (!searchParams.lang) { - redirect("/race"); - } - const isValidLang = isValidLanguage(searchParams.lang); - - if (!isValidLang) { - redirect("/race"); - } - - const user = await getCurrentUser(); - - return ( -
- -
- ); -} diff --git a/packages/app/src/app/race/(play)/multiplayer/room.tsx b/packages/app/src/app/race/(play)/multiplayer/room.tsx deleted file mode 100644 index 8e395a21..00000000 --- a/packages/app/src/app/race/(play)/multiplayer/room.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client"; - -import { Language } from "@/config/languages"; -import React, { useEffect, useState } from "react"; -import { socket } from "@/lib/socket"; - -// Types -import type { User } from "next-auth"; -import { Prisma } from "@prisma/client"; -import { RaceStatus } from "@code-racer/wss/src/types"; -import { GameStateUpdatePayload } from "@code-racer/wss/src/events/server-to-client"; - -import MultiplayerLoadingLobby from "@/app/race/_components/multiplayer-loading-lobby"; -import GameMultiplayer from "@/app/race/_components/race/game-multiplayer"; - -type Participant = Omit< - GameStateUpdatePayload["raceState"]["participants"][number], - "socketId" ->; - -export default function RaceMultiplayerRoom({ - user, - language, -}: { - user?: User; - language: Language; -}) { - const [race, setRace] = React.useState - > | null>(null); - const [raceParticipantId, setRaceParticipantId] = React.useState(""); - const [raceStatus, setRaceStatus] = React.useState(null); - const [raceStartCountdown, setRaceStartCountdown] = useState(0); - - const [participants, setParticipants] = React.useState([]); - - // Connection to wss - useEffect(() => { - socket.emit("UserGetRace", { - language, - userId: user?.id, - }); - - return () => { - socket.disconnect(); - socket.off("connect"); - }; - }, []); - - useEffect(() => { - socket.on("UserRaceResponse", (payload) => { - setRace(payload.race); - setParticipants(payload.participants); - setRaceStatus(payload.raceStatus); - setRaceParticipantId(payload.raceParticipantId); - }); - - socket.on("GameStateUpdate", (payload) => { - setParticipants(payload.raceState.participants); - setRaceStatus(payload.raceState.status); - setRaceStartCountdown(payload.raceState.countdown ?? 0); - }); - }); - - return ( - <> - {race && raceStatus !== "running" && ( - - {raceStatus === "waiting" && ( -
-
- Waiting for players -
- )} - {raceStatus === "countdown" && Boolean(raceStartCountdown) && ( -
- Game starting in: {raceStartCountdown} -
- )} -
- )} - - {race && - raceParticipantId && - (raceStatus === "running" || "finished") && ( - - )} - - ); -} diff --git a/packages/app/src/app/race/(play)/practice/loading.tsx b/packages/app/src/app/race/(play)/practice/loading.tsx new file mode 100644 index 00000000..4ae25361 --- /dev/null +++ b/packages/app/src/app/race/(play)/practice/loading.tsx @@ -0,0 +1,15 @@ +import { Loader } from "lucide-react"; +import type { NextPage } from "next"; + +const LoadingPage: NextPage = () => { + return ( +
+
+ + Fetching snippet... +
+
+ ); +}; + +export default LoadingPage; diff --git a/packages/app/src/app/race/(play)/practice/page.tsx b/packages/app/src/app/race/(play)/practice/page.tsx index 1a9f8bc0..2f624254 100644 --- a/packages/app/src/app/race/(play)/practice/page.tsx +++ b/packages/app/src/app/race/(play)/practice/page.tsx @@ -1,18 +1,20 @@ -import { getCurrentUser } from "@/lib/session"; +import type { Snippet } from ".prisma/client"; + +import React from "react"; +import Link from "next/link"; + import { getRandomSnippet } from "../loaders"; -import NoSnippet from "../../_components/no-snippet"; -import RacePractice from "../../_components/race/race-practice"; import { getSnippetById } from "../loaders"; -import { CacheBuster } from "@/components/cache-buster"; -import { Language, isValidLanguage } from "@/config/languages"; -import { redirect } from "next/navigation"; -async function getSearchParamSnippet(snippetId: string | string[]) { - if (typeof snippetId === "string") { - return await getSnippetById(snippetId); - } - return null; -} +import { getCurrentUser } from "@/lib/session"; + +import { Heading } from "@/components/ui/heading"; +import { languageTypes } from "@/lib/validations/room"; +import dynamic from "next/dynamic"; + +const RacePracticeCard = dynamic( + () => import("../../_components/practice/race-practice-card") +); type PracticeRacePageProps = { searchParams: { @@ -24,33 +26,113 @@ type PracticeRacePageProps = { export default async function PracticeRacePage({ searchParams, }: PracticeRacePageProps) { - const user = await getCurrentUser(); - const language = searchParams.lang as Language; - if (language) { - const isValidLang = isValidLanguage(language); - if (!isValidLang) { - redirect("/race"); - } - } + const session = await getCurrentUser(); + + let snippet: Snippet | null = null; - const snippet = - (await getSearchParamSnippet(searchParams.snippetId)) ?? - (await getRandomSnippet({ language: language })); + if (searchParams.snippetId && typeof searchParams.snippetId === "string") { + snippet = await getSnippetById(searchParams.snippetId); + } else if (searchParams.lang) { + const language = languageTypes.parse(searchParams.lang); + snippet = await getRandomSnippet({ + language: language, + }); + } return ( -
- - {snippet && ( -
- -
- )} - {!snippet && ( - +
+ - )} -
+ +
+ {snippet && ( + + )} + + {!snippet && ( +
+
+ +
+
+ + Create one now + +
+
+ )} + + {snippet && ( +
+ +
    +
  1. + The timer will start once you start typing on the displayed code + you see above. +
  2. +
  3. + Once you mistype a character, you must fix it before you can + continue typing. +
  4. +
  5. + You can start typing by clicking on the displayed code above. +
  6. +
  7. + The race will finish automatically once you complete the code + snippet above. +
  8. +
  9. + If you are logged in, your data will be saved on the server for + future references, such as: +
      +
    1. Your Average CPM
    2. +
    3. Your Most Used Code Languages
    4. +
    +
  10. +
  11. + When you press Enter on a ⏎ symbol (Going to a new + line), you will be able to go to the next line and the + whitespaces will automatically be typed in for you. +
  12. +
  13. You can restart since this is practice mode.
  14. +
  15. + If you click on get a new snippet and it's still the same, + then there is not enough snippet for that language. Please try + creating a new one or keep hitting the get new snippet button. +
  16. +
+
+ )} +
+ ); } diff --git a/packages/app/src/app/race/(rooms)/[roomId]/page.tsx b/packages/app/src/app/race/(rooms)/[roomId]/page.tsx deleted file mode 100644 index ec8e22c2..00000000 --- a/packages/app/src/app/race/(rooms)/[roomId]/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from "react"; - -import { Room } from "./room"; -import { getCurrentUser } from "@/lib/session"; - -export default async function RoomPage({ - params, -}: { - params: { roomId: string }; -}) { - const user = await getCurrentUser(); - - return ( -
- -
- ); -} diff --git a/packages/app/src/app/race/(rooms)/[roomId]/room.tsx b/packages/app/src/app/race/(rooms)/[roomId]/room.tsx deleted file mode 100644 index 08381779..00000000 --- a/packages/app/src/app/race/(rooms)/[roomId]/room.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import * as React from "react"; - -import { Prisma } from "@/lib/prisma"; -import { socket } from "@/lib/socket"; -import { useEffect } from "react"; -import { type RaceStatus } from "@code-racer/wss/src/types"; -import { User } from "next-auth"; -import MultiplayerLoadingLobby from "@/app/race/_components/multiplayer-loading-lobby"; -import { Button } from "@/components/ui/button"; - -import { GameStateUpdatePayload } from "@code-racer/wss/src/events/server-to-client"; -import GameMultiplayer from "../../_components/race/game-multiplayer"; -import { toast } from "@/components/ui/use-toast"; -import CopyButton from "@/components/ui/copy-button"; - -type Participant = Omit< - GameStateUpdatePayload["raceState"]["participants"][number], - "socketId" ->; - -export function Room({ user, roomId }: { user?: User; roomId: string }) { - const [race, setRace] = React.useState - > | null>(null); - const [raceParticipantId, setRaceParticipantId] = React.useState(""); - const [raceStatus, setRaceStatus] = React.useState(null); - const [raceStartCountdown, setRaceStartCountdown] = React.useState(0); - - const [participants, setParticipants] = React.useState([]); - - const notStarted = raceStatus === "waiting" || raceStatus === "countdown"; - - const isRoomLeader = raceParticipantId === participants[0]?.id; - - const canStartRace = - isRoomLeader && participants.length > 1 && raceStatus === "waiting"; - - useEffect(() => { - socket.emit("UserJoinRoom", { - userId: user?.id, - raceId: roomId, - }); - - socket.on("RoomJoined", async (payload) => { - const { race, participants, raceStatus, participantId } = payload; - - console.log(race, participants, notStarted); - - setRace(race); - setParticipants(participants); - setRaceStatus(raceStatus); - setRaceParticipantId(participantId); - }); - - socket.on("UpdateParticipants", async (payload) => { - setParticipants(payload.participants); - }); - - socket.on("GameStateUpdate", (payload) => { - setParticipants(payload.raceState.participants); - setRaceStatus(payload.raceState.status); - setRaceStartCountdown(payload.raceState.countdown ?? 0); - }); - - socket.on("SendNotification", (payload) => { - toast(payload); - }); - - return () => { - socket.disconnect(); - socket.off("connect"); - }; - }, []); - - function handleGameStart() { - socket.emit("StartRaceCountdown", { raceId: roomId }); - } - - return ( - <> - {participants && notStarted && ( - -
- {roomId} - -
- {isRoomLeader && ( - - )} - {raceStatus === "countdown" && Boolean(raceStartCountdown) && ( -
- Game starting in: {raceStartCountdown} -
- )} -
- )} - - {race && participants && !notStarted && ( - - )} - - ); -} diff --git a/packages/app/src/app/race/(rooms)/create/page.tsx b/packages/app/src/app/race/(rooms)/create/page.tsx deleted file mode 100644 index 00531f62..00000000 --- a/packages/app/src/app/race/(rooms)/create/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { CreateRoomForm } from "@/app/race/_components/create-room-form"; -import { NoHistoryButton } from "@/components/no-history-button"; - -export const dynamic = "force-dynamic"; - -export default async function CreateRoomPage({}) { - return ( -
- - - Race with friends - - Create or join a room to race with your friends in real-time. - - - - - - - - Join a Room - - - -
- ); -} diff --git a/packages/app/src/app/race/(rooms)/join/page.tsx b/packages/app/src/app/race/(rooms)/join/page.tsx deleted file mode 100644 index 978fc04a..00000000 --- a/packages/app/src/app/race/(rooms)/join/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { JoinRoomForm } from "@/app/race/_components/join-room-form"; -import { NoHistoryButton } from "@/components/no-history-button"; - -export const dynamic = "force-dynamic"; - -export default async function JoinRoomPage({}) { - return ( -
- - - Race with friends - - Create or join a room to race with your friends in real-time. - - - - - - - Create Room - - - -
- ); -} diff --git a/packages/app/src/app/race/@modal/(.)create/page.tsx b/packages/app/src/app/race/@modal/(.)create/page.tsx deleted file mode 100644 index 1a21b586..00000000 --- a/packages/app/src/app/race/@modal/(.)create/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import CloseModal from "@/components/close-modal"; - -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { CreateRoomForm } from "@/app/race/_components/create-room-form"; -import { NoHistoryButton } from "@/components/no-history-button"; - -export const dynamic = "force-dynamic"; - -export default async function CreateRoomModal() { - return ( -
-
-
-
- -
- - - Race with friends - - Create or join a room to race with your friends in real-time. - - - - - - - - Join a Room - - - -
-
-
- ); -} diff --git a/packages/app/src/app/race/@modal/(.)join/page.tsx b/packages/app/src/app/race/@modal/(.)join/page.tsx deleted file mode 100644 index e63228c0..00000000 --- a/packages/app/src/app/race/@modal/(.)join/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import CloseModal from "@/components/close-modal"; - -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { JoinRoomForm } from "@/app/race/_components/join-room-form"; -import { NoHistoryButton } from "@/components/no-history-button"; - -export const dynamic = "force-dynamic"; - -export default async function JoinRoomModal() { - return ( -
-
-
-
- -
- - - Race with friends - - Create or join a room to race with your friends in real-time. - - - - - - Create Room - - -
-
-
- ); -} diff --git a/packages/app/src/app/race/@modal/default.tsx b/packages/app/src/app/race/@modal/default.tsx deleted file mode 100644 index 581a0911..00000000 --- a/packages/app/src/app/race/@modal/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function DefaultPage() { - return null; -} \ No newline at end of file diff --git a/packages/app/src/app/race/@modal/layout.tsx b/packages/app/src/app/race/@modal/layout.tsx deleted file mode 100644 index e8303bf9..00000000 --- a/packages/app/src/app/race/@modal/layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from "react"; -import ModalCatchAllFix from "./modal-catchAll-fix"; - -export default async function ModalLaoyut({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} diff --git a/packages/app/src/app/race/@modal/modal-catchAll-fix.tsx b/packages/app/src/app/race/@modal/modal-catchAll-fix.tsx deleted file mode 100644 index 8b909b98..00000000 --- a/packages/app/src/app/race/@modal/modal-catchAll-fix.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import { usePathname } from "next/navigation"; -import * as React from "react"; - -// see https://github.com/vercel/next.js/discussions/50284 - -const ModalCatchAllFix = ({ children }: { children: React.ReactNode }) => { - const pathname = usePathname(); - - if (!(pathname.includes("/create") || pathname.includes("/join"))) { - return null; - } - - return <>{children}; -}; - -export default ModalCatchAllFix; diff --git a/packages/app/src/app/race/_components/cards/friends-race-card.tsx b/packages/app/src/app/race/_components/cards/friends-race-card.tsx deleted file mode 100644 index 3c86f731..00000000 --- a/packages/app/src/app/race/_components/cards/friends-race-card.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Button, buttonVariants } from "@/components/ui/button"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { Users } from "lucide-react"; -import { useSession } from "next-auth/react"; -import { useToast } from "@/components/ui/use-toast"; -import { bruno_ace_sc } from "@/lib/fonts"; -import Link from "next/link"; -import { cn } from "@/lib/utils"; - -export default function FriendsRaceCard({ enabled }: { enabled: boolean }) { - return ( - - -
- -
-

- Race Friends -

-

- Create your own racetrack and play with friends -

-
-
-
- - - Create Room - - - Join Room - - -
- ); -} diff --git a/packages/app/src/app/race/_components/cards/multiplayer-race-card.tsx b/packages/app/src/app/race/_components/cards/play-multiplayer-race-card.tsx similarity index 52% rename from packages/app/src/app/race/_components/cards/multiplayer-race-card.tsx rename to packages/app/src/app/race/_components/cards/play-multiplayer-race-card.tsx index 8288c504..822f01d3 100644 --- a/packages/app/src/app/race/_components/cards/multiplayer-race-card.tsx +++ b/packages/app/src/app/race/_components/cards/play-multiplayer-race-card.tsx @@ -1,92 +1,92 @@ "use client"; -import { Button, buttonVariants } from "@/components/ui/button"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; + +import React from "react"; +import { useRouter } from "next/navigation"; import { ArrowRight, Loader2, Users } from "lucide-react"; + +import LanguageDropdown from "@/app/add-snippet/_components/language-dropdown"; + import { cn } from "@/lib/utils"; -import React, { SetStateAction, useState } from "react"; -import { bruno_ace_sc } from "@/lib/fonts"; -import LanguageDropDown from "@/app/add-snippet/_components/language-dropdown"; -import { useRouter } from "next/navigation"; -import clsx from "clsx"; +import { LanguageType } from "@/lib/validations/room"; -export default function MultiplayerRaceCard({ enabled }: { enabled: boolean }) { - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(""); - const [selectedMultiplayerLanguage, setSelectedMultiplayerLanguage] = - useState(""); +import { Button, buttonVariants } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Heading } from "@/components/ui/heading"; - function handleSetCodeLanguage(props: SetStateAction) { - setSelectedMultiplayerLanguage(props); - setError(""); - } +type Props = { + enabled: boolean; +}; - function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!enabled) return; +const PlayMultiplayerRaceCard: React.FC = ({ enabled }) => { + const [language, setLanguage] = React.useState(); + const [error, setError] = React.useState(); + const [isLoading, setIsLoading] = React.useState(false); - setIsLoading(true); + const router = useRouter(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!enabled) { + return; + } setError(""); - if (!selectedMultiplayerLanguage) - return setError("please select a language to practice"); - router.push( - `/race/multiplayer?lang=${encodeURIComponent( - selectedMultiplayerLanguage, - )}`, // Make sure it is URL encoded - ); + setIsLoading(true); + if (!language) { + setError("Please select a language."); + return; + } + const searchQuery = "?lang=" + encodeURIComponent(language); + router.push("/race/multiplayer" + searchQuery); setIsLoading(false); - } + }; return (
-

- Multiplayer -

-

- Race against other people and see who can type the fastest! -

+
+
- {!enabled ? ( - - ) : ( - <> + )} + + {enabled && ( +
- {error}
- +
)}
); -} +}; + +export default React.memo(PlayMultiplayerRaceCard); diff --git a/packages/app/src/app/race/_components/cards/practice-race-card.tsx b/packages/app/src/app/race/_components/cards/practice-race-card.tsx index 925dd443..58a8e384 100644 --- a/packages/app/src/app/race/_components/cards/practice-race-card.tsx +++ b/packages/app/src/app/race/_components/cards/practice-race-card.tsx @@ -1,19 +1,33 @@ "use client"; + +import React, { Fragment, SetStateAction, useState } from "react"; + +import { z } from "zod"; +import { useRouter } from "next/navigation"; + +import { ArrowRight, Target } from "lucide-react"; import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { ArrowRight, Target } from "lucide-react"; + import { cn } from "@/lib/utils"; -import React, { SetStateAction, useState } from "react"; -import { useRouter } from "next/navigation"; + import LanguageDropDown from "@/app/add-snippet/_components/language-dropdown"; + import { bruno_ace_sc } from "@/lib/fonts"; +import { languageTypes } from "@/lib/validations/room"; + +type LanguageType = z.infer; export default function PracticeRaceCard() { - const [selectedPracticeLanguage, setSelectedPracticeLanguage] = useState(""); + const [selectedPracticeLanguage, setSelectedPracticeLanguage] = useState< + LanguageType | undefined + >(); const [error, setError] = useState(""); const router = useRouter(); - function handleSetCodeLanguage(props: SetStateAction) { + function handleSetCodeLanguage( + props: SetStateAction + ) { setSelectedPracticeLanguage(props); setError(""); } @@ -24,60 +38,63 @@ export default function PracticeRaceCard() { if (!selectedPracticeLanguage) return setError("please select a language to practice"); router.push( - `/race/practice?lang=${encodeURIComponent(selectedPracticeLanguage)}`, // Make sure it is URL encoded + `/race/practice?lang=${encodeURIComponent(selectedPracticeLanguage)}` // Make sure it is URL encoded ); } return ( - - -
- -

- Practice -

-

- Practice typing with a random snippet from your snippets -

-
-
- -
-
- - {error} + + + +
+ +

+ Practice +

+

+ Practice typing with a random snippet from your snippets +

- - - -
+
+ + {error} +
+ + + + +
); } diff --git a/packages/app/src/app/race/_components/cards/race-room-card.tsx b/packages/app/src/app/race/_components/cards/race-room-card.tsx new file mode 100644 index 00000000..9e07e748 --- /dev/null +++ b/packages/app/src/app/race/_components/cards/race-room-card.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import Link from "next/link"; +import { Users } from "lucide-react"; + +import { Heading } from "@/components/ui/heading"; +import { buttonVariants } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; + +import { cn } from "@/lib/utils"; + +const RaceRoomCard: React.FC = () => { + return ( + + +
+ + +
+
+ + + Go now! + + +
+ ); +}; + +export default React.memo(RaceRoomCard); diff --git a/packages/app/src/app/race/_components/common/progress-tracker.tsx b/packages/app/src/app/race/_components/common/progress-tracker.tsx new file mode 100644 index 00000000..9a21a8c7 --- /dev/null +++ b/packages/app/src/app/race/_components/common/progress-tracker.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +import { ProgressBar, ProgressIndicator } from "@/components/ui/progress-bar"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; + +type Props = { + name: string; + image: string; + progress: number; +}; + +const ProgressTracker: React.FC = React.memo( + ({ name, image, progress }) => { + return ( +
+ + + + {name.charAt(0)} + + + + +
+ ); + } +); + +ProgressTracker.displayName = "ProgressTracker"; + +export default ProgressTracker; diff --git a/packages/app/src/app/race/_components/common/row-line-tracker.tsx b/packages/app/src/app/race/_components/common/row-line-tracker.tsx new file mode 100644 index 00000000..18c9111f --- /dev/null +++ b/packages/app/src/app/race/_components/common/row-line-tracker.tsx @@ -0,0 +1,36 @@ +import React from "react"; + +import { cn } from "@/lib/utils"; + +type Props = { + amountOfRows: number; + currentRowInRecursion?: number; + currentLineNumber: number; +}; + +const RowLineTracker: React.FC = React.memo( + ({ amountOfRows, currentRowInRecursion = 0, currentLineNumber }) => { + return ( + + + {currentRowInRecursion + 1} + + {currentRowInRecursion !== amountOfRows && ( + + )} + + ); + } +); + +RowLineTracker.displayName = "RowLineTracker"; +export default RowLineTracker; diff --git a/packages/app/src/app/race/_components/common/typing-card.tsx b/packages/app/src/app/race/_components/common/typing-card.tsx new file mode 100644 index 00000000..8342f700 --- /dev/null +++ b/packages/app/src/app/race/_components/common/typing-card.tsx @@ -0,0 +1,108 @@ +"use client"; + +import React from "react"; + +import { Textarea } from "@/components/ui/textarea"; +import { cn } from "@/lib/utils"; + +type Props = { + handleInputChange: (_e: React.ChangeEvent) => void; + handleKeyDownEvent: (_e: React.KeyboardEvent) => void; + input: string; + code: string; + didUserMistype: boolean; + isUserFinished?: boolean; +}; + +/** Requires a ref to focus on the TextArea element! A function handler for that is onDivClick. + * We can also have a disabler for when, for example, we report a snippet or get a new one. + * We don't want the user to continue typing when we are fetching a new snippet for them. + */ +const TypingCard = React.memo( + React.forwardRef( + ( + { handleInputChange, handleKeyDownEvent, input, code, didUserMistype, isUserFinished }, + ref + ) => { + return ( + +
+            
+          
+
+