diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a27b2e2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,41 @@ +# Dependencies +node_modules +.pnpm-store + +# Lockfile backups +pnpm-lock.yaml.bak + +# Build outputs +dist +build +.next +.turbo + +# Git +.git +.gitignore + +# Environment +.env* +!.env.example + +# Docker +.dockerignore +**/Dockerfile +docker-compose.yml + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# System Files +.DS_Store +Thumbs.db + +# Testing +coverage + +# Cache +.eslintcache \ No newline at end of file diff --git a/.github/workflows/backend-deploy.yml b/.github/workflows/backend-deploy.yml new file mode 100644 index 0000000..20fbe45 --- /dev/null +++ b/.github/workflows/backend-deploy.yml @@ -0,0 +1,77 @@ +name: Deploy Backend to Production + +on: + push: + branches: + - dont-deploy + paths: + - 'apps/api/**' + - 'packages/**' + - 'pnpm-lock.yaml' + - '.github/workflows/backend-deploy.yml' + - 'Docker/Dockerfile.api' + workflow_dispatch: # Allow manual triggering + +jobs: + deploy: + name: Build and Deploy Backend + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + - name: Build and Push Docker Image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Docker/Dockerfile.api + push: true + tags: | + ${{ secrets.DOCKER_USERNAME }}/simcasino-api:latest + ${{ secrets.DOCKER_USERNAME }}/simcasino-api:${{ github.sha }} + + - name: Deploy to EC2 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_KEY }} + script: | + # Pull the latest image + docker pull ${{ secrets.DOCKER_USERNAME }}/simcasino-api:latest + + # Stop and remove the old container + docker stop simcasino-api || true + docker rm simcasino-api || true + + # Run the new container + docker run -d \ + --name simcasino-api \ + --restart always \ + -p 5000:5000 \ + -e NODE_ENV=production \ + -e DATABASE_URL="${{ secrets.DATABASE_URL }}" \ + -e COOKIE_SECRET="${{ secrets.COOKIE_SECRET }}" \ + -e CORS_ORIGIN="${{ secrets.CORS_ORIGIN }}" \ + -e GOOGLE_CLIENT_ID="${{ secrets.GOOGLE_CLIENT_ID }}" \ + -e GOOGLE_CLIENT_SECRET="${{ secrets.GOOGLE_CLIENT_SECRET }}" \ + -e CLIENT_URL="${{ secrets.CLIENT_URL }}" \ + -e REDIRECT_URL="${{ secrets.REDIRECT_URL }}" \ + ${{ secrets.DOCKER_USERNAME }}/simcasino-api:latest + + - name: Deployment Notification + if: success() + run: | + echo "Backend successfully deployed to production." + echo "Commit: ${GITHUB_SHA::7}" + echo "Branch: ${GITHUB_REF#refs/heads/}" diff --git a/.github/workflows/frontend-deploy.yml b/.github/workflows/frontend-deploy.yml new file mode 100644 index 0000000..3d611fe --- /dev/null +++ b/.github/workflows/frontend-deploy.yml @@ -0,0 +1,87 @@ +name: Deploy Frontend to S3 and CloudFront + +on: + push: + branches: + - dont-deploy + paths: + - 'apps/frontend/**' + - 'packages/**' + - 'pnpm-lock.yaml' + - '.github/workflows/frontend-deploy.yml' + workflow_dispatch: # Allow manual triggering + +env: + NODE_VERSION: 22.14.0 # Latest LTS version + PNPM_VERSION: 9.9.0 + +jobs: + build-and-deploy: + name: Build and Deploy Frontend + runs-on: ubuntu-latest + permissions: + id-token: write # Needed for AWS OIDC authentication + contents: read + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install PNPM + run: npm install -g pnpm@${{ env.PNPM_VERSION }} + + - name: Setup Environment Variables + run: | + echo "VITE_APP_API_URL=${{ secrets.VITE_APP_API_URL }}" >> .env + echo "VITE_APP_VERSION=${GITHUB_SHA::7}" >> .env + echo "VITE_APP_ENVIRONMENT=production" >> .env + working-directory: ./apps/frontend + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Build Frontend + run: pnpm run build --filter=frontend + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Deploy to S3 + run: | + aws s3 sync ./apps/frontend/dist/ s3://${{ secrets.S3_BUCKET_NAME }}/ \ + --delete \ + --cache-control "max-age=31536000,public" \ + --exclude "*.html" \ + --exclude "robots.txt" \ + --exclude "sitemap.xml" + + # Upload HTML files with different cache settings + aws s3 sync ./apps/frontend/dist/ s3://${{ secrets.S3_BUCKET_NAME }}/ \ + --cache-control "max-age=0,no-cache,no-store,must-revalidate" \ + --content-type "text/html" \ + --exclude "*" \ + --include "*.html" \ + --include "robots.txt" \ + --include "sitemap.xml" + + - name: Invalidate CloudFront Distribution + run: | + aws cloudfront create-invalidation \ + --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \ + --paths "/*" + + - name: Deployment Notification + if: success() + run: | + echo "Frontend successfully deployed to production." + echo "Commit: ${GITHUB_SHA::7}" + echo "Branch: ${GITHUB_REF#refs/heads/}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..4ddaaf3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Linting and Formatting the PR + +on: + pull_request: + branches: + - '**' + +jobs: + Continuous-Integration: + name: Performs linting and formatting on the PR + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v3 + - name: Setup pnpm + uses: pnpm/action-setup@v2 + - name: Install dependencies + run: pnpm install + - name: Lint + run: pnpm lint + - name: Format + run: pnpm format diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1a2be0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules +.turbo +*.log +.next +dist +dist-ssr +*.local +.env +.cache +server/dist +public/dist diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..29dd12a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +# Run lint-staged to check staged files +npx lint-staged diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..ded82e2 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers = true diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8a9256f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 80, + "arrowParens": "avoid" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d44dba6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + }, + "editor.formatOnSave": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ] +} diff --git a/Docker/Dockerfile.api b/Docker/Dockerfile.api new file mode 100644 index 0000000..ace636a --- /dev/null +++ b/Docker/Dockerfile.api @@ -0,0 +1,35 @@ +FROM node:22-slim + +# 🛠 Install OpenSSL 1.1 (needed by Prisma) and other required packages +RUN apt-get update && apt-get install -y \ + openssl \ + libssl-dev \ + libstdc++6 \ + zlib1g \ + bash \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g pnpm@9.9.0 + +WORKDIR /usr/src/app + +COPY ./packages ./packages +COPY ./pnpm-lock.yaml ./pnpm-lock.yaml +COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml + +COPY ./package.json ./package.json +COPY ./tsconfig.json ./tsconfig.json +COPY ./turbo.json ./turbo.json + +COPY ./apps/api ./apps/api + +RUN pnpm install --frozen-lockfile + +# Generate Prisma client +RUN pnpm db:generate + +RUN pnpm build --filter=api + +WORKDIR /usr/src/app/apps/api + +CMD ["pnpm", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..86b80b2 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Turborepo kitchen sink starter + +This is an official starter Turborepo with multiple meta-frameworks all working in harmony and sharing packages. + +This example also shows how to use [Workspace Configurations](https://turbo.build/repo/docs/core-concepts/monorepos/configuring-workspaces). + +## Using this example + +Run the following command: + +```sh +npx create-turbo@latest -e kitchen-sink +``` + +## What's inside? + +This Turborepo includes the following packages and apps: + +### Apps and Packages + +- `api`: an [Express](https://expressjs.com/) server +- `storefront`: a [Next.js](https://nextjs.org/) app +- `admin`: a [Vite](https://vitejs.dev/) single page app +- `blog`: a [Remix](https://remix.run/) blog +- `@repo/eslint-config`: ESLint configurations used throughout the monorepo +- `@repo/jest-presets`: Jest configurations +- `@repo/logger`: isomorphic logger (a small wrapper around console.log) +- `@repo/ui`: a dummy React UI library (which contains `` and `` components) +- `@repo/typescript-config`: tsconfig.json's used throughout the monorepo + +Each package and app is 100% [TypeScript](https://www.typescriptlang.org/). + +### Utilities + +This Turborepo has some additional tools already setup for you: + +- [TypeScript](https://www.typescriptlang.org/) for static type checking +- [ESLint](https://eslint.org/) for code linting +- [Jest](https://jestjs.io) test runner for all things JavaScript +- [Prettier](https://prettier.io) for code formatting diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..4451a71 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,12 @@ +PORT=5000 + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +CLIENT_URL=http://localhost:3000 +REDIRECT_URL=/api/v1/auth/google/callback + +COOKIE_SECRET=secret + +NODE_ENV=development + +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres diff --git a/apps/api/.eslintrc.js b/apps/api/.eslintrc.js new file mode 100644 index 0000000..cf2a84b --- /dev/null +++ b/apps/api/.eslintrc.js @@ -0,0 +1,12 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ['@repo/eslint-config/server.js'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + }, + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-misused-promises': 'off', + }, +}; diff --git a/apps/api/dice.svg b/apps/api/dice.svg new file mode 100644 index 0000000..3a84b41 --- /dev/null +++ b/apps/api/dice.svg @@ -0,0 +1 @@ +2Artboard 440 \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..0aa55ef --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,57 @@ +{ + "name": "api", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node dist/index.js", + "dev": "tsup --watch --onSuccess \"node dist/index.js\"", + "build": "tsup", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit", + "lint": "eslint src/", + "test": "jest --detectOpenHandles" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "dependencies": { + "@prisma/client": "5.19.0", + "bcrypt": "^5.1.1", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.18.3", + "express-async-errors": "^3.1.1", + "express-session": "^1.18.0", + "http-status-codes": "^2.3.0", + "lodash": "^4.17.21", + "morgan": "^1.10.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-local": "^1.0.0" + }, + "devDependencies": { + "@repo/common": "workspace:*", + "@repo/db": "workspace:*", + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/bcrypt": "^5.0.2", + "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", + "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.7", + "@types/morgan": "^1.9.9", + "@types/node": "^20.11.24", + "@types/passport": "^1.0.16", + "@types/passport-google-oauth20": "^2.0.16", + "@types/passport-local": "^1.0.38", + "@types/supertest": "^6.0.2", + "jest": "^29.7.0", + "supertest": "^6.3.4", + "tsup": "^8.0.2", + "typescript": "^5.3.3" + } +} diff --git a/apps/api/src/__tests__/server.test.ts b/apps/api/src/__tests__/server.test.ts new file mode 100644 index 0000000..f3a642a --- /dev/null +++ b/apps/api/src/__tests__/server.test.ts @@ -0,0 +1,22 @@ +import supertest from 'supertest'; +import { createServer } from '../server'; + +describe('Server', () => { + it('health check returns 200', async () => { + await supertest(createServer()) + .get('/status') + .expect(200) + .then(res => { + expect(res.ok).toBe(true); + }); + }); + + it('message endpoint says hello', async () => { + await supertest(createServer()) + .get('/message/jared') + .expect(200) + .then(res => { + expect(res.body).toEqual({ message: 'hello jared' }); + }); + }); +}); diff --git a/apps/api/src/config/passport.ts b/apps/api/src/config/passport.ts new file mode 100644 index 0000000..f89fb90 --- /dev/null +++ b/apps/api/src/config/passport.ts @@ -0,0 +1,86 @@ +import passport from 'passport'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +import { Strategy as LocalStrategy } from 'passport-local'; +import { compare } from 'bcrypt'; +import db from '@repo/db'; +import type { User } from '@prisma/client'; + +passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID || '', + clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', + callbackURL: '/api/v1/auth/google/callback', + }, + async (_, __, profile, done) => { + const profileInfo = { + googleId: profile.id, + name: profile.displayName, + picture: profile.photos?.[0].value || null, + }; + + try { + // Find the user by email + let user = await db.user.findFirst({ + where: { email: profile.emails?.[0].value || '' }, + }); + if (!user) { + user = await db.user.create({ + data: { + ...profileInfo, + email: profile.emails?.[0].value || '', + }, + }); + } else { + user = await db.user.update({ + where: { id: user.id }, + data: profileInfo, + }); + } + + done(null, user); + } catch (err) { + done(err, undefined); + } + } + ) +); + +passport.use( + new LocalStrategy( + { + usernameField: 'email', + }, + async (email, password, done) => { + try { + const user = await db.user.findUnique({ + where: { email }, + }); + + if (!user || !(await compare(password, user.password || ''))) { + done(null, false, { message: 'Invalid email or password' }); + return; + } + + done(null, user); + } catch (err) { + done(err, undefined); + } + } + ) +); + +passport.serializeUser((user: Express.User, done) => { + done(null, (user as User).id); +}); + +passport.deserializeUser(async (id: string, done) => { + try { + const user = await db.user.findUnique({ + where: { id }, + }); + done(null, user); + } catch (err) { + done(err, null); + } +}); diff --git a/apps/api/src/errors/bad-request.ts b/apps/api/src/errors/bad-request.ts new file mode 100644 index 0000000..8acc12f --- /dev/null +++ b/apps/api/src/errors/bad-request.ts @@ -0,0 +1,12 @@ +import { StatusCodes } from 'http-status-codes'; +import CustomAPIError from './custom-api'; + +class BadRequestError extends CustomAPIError { + statusCode: StatusCodes; + constructor(message: string) { + super(message); + this.statusCode = StatusCodes.BAD_REQUEST; + } +} + +export default BadRequestError; diff --git a/apps/api/src/errors/custom-api.ts b/apps/api/src/errors/custom-api.ts new file mode 100644 index 0000000..c52f62c --- /dev/null +++ b/apps/api/src/errors/custom-api.ts @@ -0,0 +1,3 @@ +class CustomAPIError extends Error {} + +export default CustomAPIError; diff --git a/apps/api/src/errors/index.ts b/apps/api/src/errors/index.ts new file mode 100644 index 0000000..1ec8aee --- /dev/null +++ b/apps/api/src/errors/index.ts @@ -0,0 +1,5 @@ +import BadRequestError from './bad-request'; +import NotFoundError from './not-found'; +import UnAuthenticatedError from './unauthenticated'; + +export { BadRequestError, NotFoundError, UnAuthenticatedError }; diff --git a/apps/api/src/errors/not-found.ts b/apps/api/src/errors/not-found.ts new file mode 100644 index 0000000..dd8865e --- /dev/null +++ b/apps/api/src/errors/not-found.ts @@ -0,0 +1,12 @@ +import { StatusCodes } from 'http-status-codes'; +import CustomAPIError from './custom-api'; + +class NotFoundError extends CustomAPIError { + statusCode: StatusCodes; + constructor(message: string) { + super(message); + this.statusCode = StatusCodes.NOT_FOUND; + } +} + +export default NotFoundError; diff --git a/apps/api/src/errors/unauthenticated.ts b/apps/api/src/errors/unauthenticated.ts new file mode 100644 index 0000000..fbaf366 --- /dev/null +++ b/apps/api/src/errors/unauthenticated.ts @@ -0,0 +1,12 @@ +import { StatusCodes } from 'http-status-codes'; +import CustomAPIError from './custom-api'; + +class UnAuthenticatedError extends CustomAPIError { + statusCode: StatusCodes; + constructor(message: string) { + super(message); + this.statusCode = StatusCodes.UNAUTHORIZED; + } +} + +export default UnAuthenticatedError; diff --git a/apps/api/src/features/auth/auth.router.ts b/apps/api/src/features/auth/auth.router.ts new file mode 100644 index 0000000..40b5d72 --- /dev/null +++ b/apps/api/src/features/auth/auth.router.ts @@ -0,0 +1,99 @@ +import passport from 'passport'; +import { hash } from 'bcrypt'; +import db from '@repo/db'; +import type { User } from '@prisma/client'; +import { StatusCodes } from 'http-status-codes'; +import { ApiResponse } from '@repo/common/types'; +import { Router } from 'express'; +import type { RequestHandler } from 'express'; +import { BadRequestError } from '../../errors'; +import { isAuthenticated } from '../../middlewares/auth.middleware'; + +interface RegisterRequestBody { + email: string; + password: string; + name: string; +} + +const router: Router = Router(); + +// Google authentication routes +router.get('/google', (req, res, next) => { + const state = JSON.stringify({ redirect: req.query.redirect_to }); + // Store the redirect URL in session if provided + ( + passport.authenticate('google', { + scope: ['profile', 'email'], + state: encodeURIComponent(state), + }) as RequestHandler + )(req, res, next); +}); + +router.get( + '/google/callback', + passport.authenticate('google', { + failureRedirect: `${process.env.CLIENT_URL}/login`, + }) as RequestHandler, + (req, res) => { + const state = req.query.state + ? (JSON.parse(decodeURIComponent(req.query.state as string)) as { + redirect?: string; + }) + : {}; + res.redirect(state.redirect || `${process.env.CLIENT_URL}`); + } +); + +// Local authentication routes +router.post( + '/login', + passport.authenticate('local', { + failureRedirect: `${process.env.CLIENT_URL}/login`, + }) as RequestHandler, + (req, res) => { + res.redirect(`${process.env.CLIENT_URL}`); + } +); + +router.post('/register', async (req, res) => { + const { email, password, name } = req.body as RegisterRequestBody; + + const hashedPassword = await hash(password, 10); + const user = await db.user.upsert({ + where: { email }, + update: { + password: hashedPassword, + name, + }, + create: { + email, + password: hashedPassword, + name, + }, + }); + + req.login(user, err => { + if (err) throw new BadRequestError('Error logging in'); + res.redirect(`${process.env.CLIENT_URL}`); + }); +}); + +router.get('/logout', (req, res, next) => { + req.logout(err => { + if (err) next(err); + res.redirect('/auth'); + }); +}); + +router.get('/me', isAuthenticated, (req, res) => { + const user = req.user as User; + if (user.password) { + const { password: _password, ...userWithoutPassword } = user; + return res + .status(StatusCodes.OK) + .json(new ApiResponse(StatusCodes.OK, userWithoutPassword)); + } + return res.status(StatusCodes.OK).json(new ApiResponse(StatusCodes.OK, user)); +}); + +export default router; diff --git a/apps/api/src/features/games/blackjack/blackjack.controller.ts b/apps/api/src/features/games/blackjack/blackjack.controller.ts new file mode 100644 index 0000000..b431f16 --- /dev/null +++ b/apps/api/src/features/games/blackjack/blackjack.controller.ts @@ -0,0 +1,125 @@ +import type { User } from '@prisma/client'; +import { ApiResponse } from '@repo/common/types'; +import type { Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { + BlackjackBetSchema, + BlackjackPlayRoundSchema, +} from '@repo/common/game-utils/blackjack/validations.js'; +import type { + BlackjackActions, + BlackjackPlayRoundResponse, +} from '@repo/common/game-utils/blackjack/types.js'; +import db from '@repo/db'; +import { userManager } from '../../user/user.service'; +import { blackjackManager } from './blackjack.service'; + +export const placeBet = async ( + req: Request, + res: Response | { message: string }> +) => { + const { betAmount } = validateBetRequest(req.body as { betAmount: number }); + const user = req.user as User; + + await validateActiveBet(user.id); + + await validateUserBalance(user.id, betAmount); + + const game = await blackjackManager.createGame({ + betAmount: Math.round(betAmount * 100), // Convert to cents + userId: user.id, + }); + + const dbUpdateObject = game.getDbUpdateObject(); + + if (dbUpdateObject) { + await db.bet.update(dbUpdateObject); + if ('active' in dbUpdateObject.data) { + blackjackManager.deleteGame(user.id); + } + } + + res + .status(StatusCodes.OK) + .json(new ApiResponse(StatusCodes.OK, game.getPlayRoundResponse())); +}; + +export const getActiveGame = async ( + req: Request, + res: Response | { message: string }> +) => { + const userId = (req.user as User).id; + const game = await blackjackManager.getGame(userId); + + if (!game || !game.getBet().active) { + return res + .status(StatusCodes.NOT_FOUND) + .json(new ApiResponse(StatusCodes.NOT_FOUND, {}, 'Game not found')); + } + + return res + .status(StatusCodes.OK) + .json(new ApiResponse(StatusCodes.OK, game.getPlayRoundResponse())); +}; + +export const blackjackNext = async ( + req: Request, + res: Response | { message: string }> +) => { + const { action } = validatePlayRequest(req.body as { action: string }); + const userId = (req.user as User).id; + const game = await blackjackManager.getGame(userId); + + if (!game?.getBet().active) { + return res + .status(StatusCodes.BAD_REQUEST) + .json(new ApiResponse(StatusCodes.BAD_REQUEST, {}, 'Game not found')); + } + + game.playRound(action as BlackjackActions); + const dbUpdateObject = game.getDbUpdateObject(); + + if (dbUpdateObject) { + if ('active' in dbUpdateObject.data) { + blackjackManager.deleteGame(userId); + } + await db.bet.update(dbUpdateObject); + } + + return res + .status(StatusCodes.OK) + .json(new ApiResponse(StatusCodes.OK, game.getPlayRoundResponse())); +}; + +const validateBetRequest = (body: { betAmount: number }) => { + const result = BlackjackBetSchema.safeParse(body); + if (!result.success) { + throw new Error(result.error.message); + } + return result.data; +}; + +const validateUserBalance = async (userId: string, betAmount: number) => { + const userInstance = await userManager.getUser(userId); + const betAmountInCents = Math.round(betAmount * 100); + const userBalanceInCents = userInstance.getBalanceAsNumber(); + + if (userBalanceInCents < betAmountInCents) { + throw new Error('Insufficient balance'); + } +}; + +const validateActiveBet = async (userId: string) => { + const game = await blackjackManager.getGame(userId); + if (game && game.getBet().active) { + throw new Error('You already have an active bet'); + } +}; + +const validatePlayRequest = (body: { action: string }) => { + const result = BlackjackPlayRoundSchema.safeParse(body); + if (!result.success) { + throw new Error(result.error.message); + } + return result.data; +}; diff --git a/apps/api/src/features/games/blackjack/blackjack.router.ts b/apps/api/src/features/games/blackjack/blackjack.router.ts new file mode 100644 index 0000000..289027a --- /dev/null +++ b/apps/api/src/features/games/blackjack/blackjack.router.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { isAuthenticated } from '../../../middlewares/auth.middleware'; +import { blackjackNext, getActiveGame, placeBet } from './blackjack.controller'; + +const blackjackRouter: Router = Router(); + +blackjackRouter.post('/bet', isAuthenticated, placeBet); +blackjackRouter.post('/next', isAuthenticated, blackjackNext); +blackjackRouter.get('/active', isAuthenticated, getActiveGame); + +export default blackjackRouter; diff --git a/apps/api/src/features/games/blackjack/blackjack.service.ts b/apps/api/src/features/games/blackjack/blackjack.service.ts new file mode 100644 index 0000000..172e9dd --- /dev/null +++ b/apps/api/src/features/games/blackjack/blackjack.service.ts @@ -0,0 +1,252 @@ +import type { Bet } from '@prisma/client'; +import db from '@repo/db'; +import { + convertFloatsToGameEvents, + determineWinner, + isActionValid, + getIsPlayerTurnOver, + playRoundAndUpdateState, + createInitialGameState, + getSafeGameState, +} from '@repo/common/game-utils/blackjack/utils.js'; +import type { + BlackjackActions, + BlackjackGameState, +} from '@repo/common/game-utils/blackjack/types.js'; +import type { UserInstance } from '../../user/user.service'; +import { userManager } from '../../user/user.service'; + +interface GameCreationParams { + userId: string; + betAmount: number; +} + +class BlackjackManager { + private static instance: BlackjackManager | undefined; + private games = new Map(); + + private constructor() { + // Initialize any necessary state + } + + static getInstance() { + if (!BlackjackManager.instance) { + BlackjackManager.instance = new BlackjackManager(); + } + return BlackjackManager.instance; + } + + async getGame(userId: string): Promise { + if (this.games.has(userId)) { + return this.games.get(userId) || null; + } + + const bet = await this.findActiveBet(userId); + if (!bet) return null; + + const game = await this.createGameFromBet(bet); + this.games.set(userId, game); + return game; + } + + async createGame({ betAmount, userId }: GameCreationParams) { + const userInstance = await userManager.getUser(userId); + const gameEvents = this.generateGameEvents(userInstance); + + const bet = await this.createBetTransaction(userInstance, betAmount); + const game = new BlackjackGame({ bet, gameEvents }); + + this.games.set(bet.userId, game); + return game; + } + + private async findActiveBet(userId: string): Promise { + return db.bet.findFirst({ + where: { userId, active: true, game: 'blackjack' }, + }); + } + + private async createGameFromBet(bet: Bet): Promise { + const userInstance = await userManager.getUser(bet.userId); + const gameEvents = this.generateGameEvents(userInstance); + return new BlackjackGame({ bet, gameEvents }); + } + + private generateGameEvents(userInstance: UserInstance): number[] { + const floats = userInstance.generateFloats(52); + return convertFloatsToGameEvents(floats); + } + + private async createBetTransaction( + userInstance: UserInstance, + betAmount: number + ): Promise { + return db.$transaction(async tx => { + const bet = await tx.bet.create({ + data: { + active: true, + betAmount, + betNonce: userInstance.getNonce(), + game: 'blackjack', + provablyFairStateId: userInstance.getProvablyFairStateId(), + state: { actions: [['deal']] }, + userId: userInstance.getUser().id, + payoutAmount: 0, + }, + }); + await userInstance.updateNonce(tx); + return bet; + }); + } + + deleteGame(userId: string) { + this.games.delete(userId); + } +} + +class BlackjackGame { + private bet: Bet; + readonly gameEvents: number[]; + private amountMultiplier = 1; + private drawIndex = 4; // Start after initial deal + private gameState: BlackjackGameState; + private payout = 0; + private active = false; + + constructor({ bet, gameEvents }: { bet: Bet; gameEvents: number[] }) { + this.bet = bet; + this.gameEvents = gameEvents; + this.gameState = createInitialGameState(gameEvents); + this.active = true; + if (this.isPlayerTurnComplete()) { + this.resolveGame(); + } + } + + getGameState() { + return this.gameState; + } + + getSafeGameState() { + return getSafeGameState(this.gameState); + } + + getBet() { + return this.bet; + } + + getAmountMultiplier(): number { + return this.amountMultiplier; + } + + playRound(action: BlackjackActions) { + this.validateAction(action); + this.executeAction(action); + if (this.isPlayerTurnComplete()) { + this.resolveGame(); + } + } + + getDbUpdateObject(isGameCreate = false): null | { + where: { id: string }; + data: + | { + state: BlackjackActions[][]; + } + | { + active: false; + payoutAmount: number; + state: BlackjackActions[][]; + }; + } { + const playerActions = this.gameState.player.map(hand => hand.actions); + + if (isGameCreate && this.active) { + return null; + } + + if (this.active) { + return { + where: { id: this.bet.id }, + data: { + state: playerActions, + }, + }; + } + return { + where: { id: this.bet.id }, + data: { + active: false, + payoutAmount: this.payout, + state: playerActions, + }, + }; + } + + private validateAction(action: BlackjackActions): void { + if ( + !isActionValid({ gameState: this.gameState, action, active: this.active }) + ) { + throw new Error(`Invalid action: ${action}`); + } + } + + private executeAction(action: BlackjackActions): void { + const result = playRoundAndUpdateState({ + gameEvents: this.gameEvents, + drawIndex: this.drawIndex, + gameState: this.gameState, + action, + amountMultiplier: this.amountMultiplier, + }); + + this.drawIndex = result.drawIndex; + this.amountMultiplier = result.amountMultiplier || this.amountMultiplier; + } + + private isPlayerTurnComplete(): boolean { + return getIsPlayerTurnOver( + this.gameState.player.map(({ actions }) => actions) + ); + } + + getPlayRoundResponse() { + if (!this.active) { + return this.constructGameOverResponse(); + } + return this.constructPlayRoundResponse(); + } + + private constructPlayRoundResponse() { + return { + id: this.bet.id, + active: this.active, + state: getSafeGameState(this.gameState), + betAmount: this.bet.betAmount / 100, // Convert back to dollars + amountMultiplier: this.amountMultiplier, + }; + } + + private constructGameOverResponse() { + return { + id: this.bet.id, + active: this.active, + state: this.gameState, + betAmount: this.bet.betAmount / 100, // Convert back to dollars + amountMultiplier: this.amountMultiplier, + payout: this.payout / 100, // Convert back to dollars + payoutMultiplier: + this.payout / (this.bet.betAmount * this.amountMultiplier), + }; + } + + private resolveGame(): void { + const payout = determineWinner(this.gameState, this.bet.betAmount); + + const finalAmount = this.bet.betAmount * this.amountMultiplier + payout; + this.payout = finalAmount; + this.active = false; + } +} + +export const blackjackManager = BlackjackManager.getInstance(); diff --git a/apps/api/src/features/games/dice/dice.controller.ts b/apps/api/src/features/games/dice/dice.controller.ts new file mode 100644 index 0000000..cd2d764 --- /dev/null +++ b/apps/api/src/features/games/dice/dice.controller.ts @@ -0,0 +1,92 @@ +import type { User } from '@prisma/client'; +import type { + DiceCondition, + DicePlaceBetResponse, +} from '@repo/common/game-utils/dice/types.js'; +import type { Request, Response } from 'express'; +import db from '@repo/db'; +import { ApiResponse } from '@repo/common/types'; +import { StatusCodes } from 'http-status-codes'; +import { BadRequestError } from '../../../errors'; +import { userManager } from '../../user/user.service'; +import { getResult } from './dice.service'; + +interface DiceRequestBody { + target: number; + condition: DiceCondition; + betAmount: number; +} + +export const placeBet = async ( + req: Request, + res: Response> +) => { + const { target, condition, betAmount } = req.body as DiceRequestBody; + + if (betAmount <= 0) { + throw new BadRequestError('Bet amount must be greater than 0'); + } + + const userInstance = await userManager.getUser((req.user as User).id); + const user = userInstance.getUser(); + + // Convert betAmount to cents for comparison with balance (which is stored in cents) + const betAmountInCents = Math.round(betAmount * 100); + const userBalanceInCents = userInstance.getBalanceAsNumber(); + + if (userBalanceInCents < betAmountInCents) { + throw new BadRequestError('Insufficient balance'); + } + + const result = getResult({ userInstance, target, condition }); + + const { payoutMultiplier } = result; + // Calculate payout in cents + const payoutInCents = + payoutMultiplier > 0 ? Math.round(betAmountInCents * payoutMultiplier) : 0; + const balanceChangeInCents = payoutInCents - betAmountInCents; + + // Update balance and create bet in a single transaction + const { balance, id } = await db.$transaction(async tx => { + // Create bet record with amounts in cents + const bet = await tx.bet.create({ + data: { + active: false, + betAmount: betAmountInCents, + betNonce: userInstance.getNonce(), + game: 'dice', + payoutAmount: payoutInCents, + provablyFairStateId: userInstance.getProvablyFairStateId(), + state: result.state, + userId: user.id, + }, + }); + + await userInstance.updateNonce(tx); + + // Calculate new balance as a string + const newBalance = (userBalanceInCents + balanceChangeInCents).toString(); + + // Update user balance with the balance change in cents + const userWithNewBalance = await tx.user.update({ + where: { id: user.id }, + data: { + balance: newBalance, + }, + }); + + return { balance: userWithNewBalance.balance, id: bet.id }; + }); + + // Update the user instance with new balance + userInstance.setBalance(balance); + + res.status(StatusCodes.OK).json( + new ApiResponse(StatusCodes.OK, { + ...result, + balance: userInstance.getBalanceAsNumber() / 100, // Convert back to dollars for the response + id, + payout: payoutInCents / 100, // Convert back to dollars for the response + }) + ); +}; diff --git a/apps/api/src/features/games/dice/dice.router.ts b/apps/api/src/features/games/dice/dice.router.ts new file mode 100644 index 0000000..17ed874 --- /dev/null +++ b/apps/api/src/features/games/dice/dice.router.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { isAuthenticated } from '../../../middlewares/auth.middleware'; +import { placeBet } from './dice.controller'; + +const diceRouter: Router = Router(); + +diceRouter.post('/place-bet', isAuthenticated, placeBet); + +export default diceRouter; diff --git a/apps/api/src/features/games/dice/dice.service.ts b/apps/api/src/features/games/dice/dice.service.ts new file mode 100644 index 0000000..94e311d --- /dev/null +++ b/apps/api/src/features/games/dice/dice.service.ts @@ -0,0 +1,53 @@ +import { + calculateMultiplier, + type DiceCondition, +} from '@repo/common/game-utils/dice/index.js'; +import type { UserInstance } from '../../user/user.service'; + +const getPayoutMultiplier = ({ + condition, + target, + result, +}: { + condition: DiceCondition; + target: number; + result: number; +}): number => { + const multiplier = calculateMultiplier(target, condition); + switch (condition) { + case 'above': + return result > target ? multiplier : 0; + case 'below': + return result < target ? multiplier : 0; + default: + return 0; + } +}; + +export const getResult = ({ + target, + condition, + userInstance, +}: { + target: number; + condition: DiceCondition; + userInstance: UserInstance; +}) => { + const [float] = userInstance.generateFloats(1); + const result = (float * 10001) / 100; // 0.00 to 100.00 + + const payoutMultiplier = getPayoutMultiplier({ + result, + condition, + target, + }); + + return { + state: { + target, + condition, + result: parseFloat(result.toFixed(2)), + }, + payoutMultiplier, + }; +}; diff --git a/apps/api/src/features/games/games.router.ts b/apps/api/src/features/games/games.router.ts new file mode 100644 index 0000000..0ffe087 --- /dev/null +++ b/apps/api/src/features/games/games.router.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; +import plinkooRouter from './plinkoo/plinkoo.router'; +import minesRouter from './mines/mines.router'; +import limboRouter from './limbo/limbo.router'; +import kenoRouter from './keno/keno.router'; +import diceRouter from './dice/dice.router'; +import rouletteRouter from './roulette/roulette.router'; +import blackjackRouter from './blackjack/blackjack.router'; + +const gameRouter: Router = Router(); + +gameRouter.use('/plinkoo', plinkooRouter); +gameRouter.use('/mines', minesRouter); +gameRouter.use('/limbo', limboRouter); +gameRouter.use('/keno', kenoRouter); +gameRouter.use('/dice', diceRouter); +gameRouter.use('/roulette', rouletteRouter); +gameRouter.use('/blackjack', blackjackRouter); + +export default gameRouter; diff --git a/apps/api/src/features/games/keno/keno.controller.ts b/apps/api/src/features/games/keno/keno.controller.ts new file mode 100644 index 0000000..09a2ee2 --- /dev/null +++ b/apps/api/src/features/games/keno/keno.controller.ts @@ -0,0 +1,84 @@ +import type { Request, Response } from 'express'; +import type { KenoResponse } from '@repo/common/game-utils/keno/types.js'; +import { KenoRequestSchema } from '@repo/common/game-utils/keno/types.js'; +import type { User } from '@prisma/client'; +import db from '@repo/db'; +import { StatusCodes } from 'http-status-codes'; +import { ApiResponse } from '@repo/common/types'; +import { BadRequestError } from '../../../errors'; +import { userManager } from '../../user/user.service'; +import { getResult } from './keno.service'; + +export const placeBet = async ( + req: Request, + res: Response> +) => { + const parsedRequest = KenoRequestSchema.safeParse(req.body); + if (!parsedRequest.success) { + throw new BadRequestError('Invalid request body'); + } + const { betAmount, selectedTiles, risk } = parsedRequest.data; + + const userInstance = await userManager.getUser((req.user as User).id); + const user = userInstance.getUser(); + + const betAmountInCents = Math.round(betAmount * 100); + + const userBalanceInCents = userInstance.getBalanceAsNumber(); + + if (userBalanceInCents < betAmountInCents) { + throw new BadRequestError('Insufficient balance'); + } + + const result = getResult({ userInstance, selectedTiles, risk }); + + const { payoutMultiplier } = result; + + const payoutInCents = + payoutMultiplier > 0 ? Math.round(betAmountInCents * payoutMultiplier) : 0; + const balanceChangeInCents = payoutInCents - betAmountInCents; + + // Update balance and create bet in a single transaction + const { balance, id } = await db.$transaction(async tx => { + // Create bet record with amounts in cents + const bet = await tx.bet.create({ + data: { + active: false, + betAmount: betAmountInCents, + betNonce: userInstance.getNonce(), + game: 'keno', + payoutAmount: payoutInCents, + provablyFairStateId: userInstance.getProvablyFairStateId(), + state: result.state, + userId: user.id, + }, + }); + + await userInstance.updateNonce(tx); + + // Calculate new balance as a string + const newBalance = (userBalanceInCents + balanceChangeInCents).toString(); + + // Update user balance with the balance change in cents + const userWithNewBalance = await tx.user.update({ + where: { id: user.id }, + data: { + balance: newBalance, + }, + }); + + return { balance: userWithNewBalance.balance, id: bet.id }; + }); + + // Update the user instance with new balance + userInstance.setBalance(balance); + + res.status(StatusCodes.OK).json( + new ApiResponse(StatusCodes.OK, { + ...result, + balance: userInstance.getBalanceAsNumber() / 100, // Convert back to dollars for the response + id, + payout: payoutInCents / 100, // Convert back to dollars for the response + }) + ); +}; diff --git a/apps/api/src/features/games/keno/keno.router.ts b/apps/api/src/features/games/keno/keno.router.ts new file mode 100644 index 0000000..e6fe416 --- /dev/null +++ b/apps/api/src/features/games/keno/keno.router.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { isAuthenticated } from '../../../middlewares/auth.middleware'; +import { placeBet } from './keno.controller'; + +const kenoRouter: Router = Router(); + +kenoRouter.post('/place-bet', isAuthenticated, placeBet); + +export default kenoRouter; diff --git a/apps/api/src/features/games/keno/keno.service.ts b/apps/api/src/features/games/keno/keno.service.ts new file mode 100644 index 0000000..ae04720 --- /dev/null +++ b/apps/api/src/features/games/keno/keno.service.ts @@ -0,0 +1,56 @@ +import type { KenoRisk } from '@repo/common/game-utils/keno/types.js'; +import { + NO_OF_TILES_KENO, + PAYOUT_MULTIPLIERS, +} from '@repo/common/game-utils/keno/constants.js'; +import { calculateSelectedGems } from '@repo/common/game-utils/keno/utils.js'; +import { convertFloatsToGameEvents } from '@repo/common/game-utils/mines/utils.js'; +import type { UserInstance } from '../../user/user.service'; + +const getPayoutMultiplier = ( + drawnNumbers: number[], + selectedTiles: number[], + risk: KenoRisk +) => { + const drawnNumbersSet = new Set(drawnNumbers); + let matches = 0; + for (const tile of selectedTiles) { + if (drawnNumbersSet.has(tile)) { + matches++; + } + } + return PAYOUT_MULTIPLIERS[risk][selectedTiles.length][matches]; +}; + +export const getResult = ({ + userInstance, + selectedTiles, + risk, +}: { + userInstance: UserInstance; + selectedTiles: number[]; + risk: KenoRisk; +}) => { + const floats = userInstance.generateFloats(10); + + const gameEvents = convertFloatsToGameEvents(floats, NO_OF_TILES_KENO); + + const drawnNumbers = calculateSelectedGems(gameEvents, 10).map( + num => num + 1 + ); + + const payoutMultiplier = getPayoutMultiplier( + drawnNumbers, + selectedTiles, + risk + ); + + return { + state: { + risk, + selectedTiles, + drawnNumbers, + }, + payoutMultiplier, + }; +}; diff --git a/apps/api/src/features/games/limbo/limbo.controller.ts b/apps/api/src/features/games/limbo/limbo.controller.ts new file mode 100644 index 0000000..8296540 --- /dev/null +++ b/apps/api/src/features/games/limbo/limbo.controller.ts @@ -0,0 +1,14 @@ +import type { Request, Response } from 'express'; +import { getResult } from './limbo.service'; + +interface LimboRequestBody { + clientSeed: string; +} + +export const placeBet = (req: Request, res: Response) => { + const { clientSeed } = req.body as LimboRequestBody; + + const result = getResult(clientSeed); + + res.status(200).json({ result }); +}; diff --git a/apps/api/src/features/games/limbo/limbo.router.ts b/apps/api/src/features/games/limbo/limbo.router.ts new file mode 100644 index 0000000..ddf789e --- /dev/null +++ b/apps/api/src/features/games/limbo/limbo.router.ts @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import { placeBet } from './limbo.controller'; + +const limboRouter: Router = Router(); + +limboRouter.post('/place-bet', placeBet); + +export default limboRouter; diff --git a/apps/api/src/features/games/limbo/limbo.service.ts b/apps/api/src/features/games/limbo/limbo.service.ts new file mode 100644 index 0000000..dbd77f8 --- /dev/null +++ b/apps/api/src/features/games/limbo/limbo.service.ts @@ -0,0 +1,16 @@ +const HOUSE_EDGE = 0.99; + +export const getResult = () => { + // const floats = rng.generateFloats({ + // clientSeed, + // count: 1, + // }); + const floats = [2]; + const floatPoint = (1e8 / (floats[0] * 1e8 + 1)) * HOUSE_EDGE; + + // Crash point rounded down to required denominator + const crashPoint = Math.floor(floatPoint * 100) / 100; + + // Consolidate all crash points below 1 + return Math.max(crashPoint, 1); +}; diff --git a/apps/api/src/features/games/mines/mines.constant.ts b/apps/api/src/features/games/mines/mines.constant.ts new file mode 100644 index 0000000..bb63e7b --- /dev/null +++ b/apps/api/src/features/games/mines/mines.constant.ts @@ -0,0 +1,331 @@ +// Check out the gist for the calculation: https://gist.github.com/nimit9/57309f2f9cc365ac090aef69e669bb6d +export const payouts: Record> = { + 1: { + 1: 1.03, + 2: 1.08, + 3: 1.12, + 4: 1.18, + 5: 1.24, + 6: 1.3, + 7: 1.37, + 8: 1.46, + 9: 1.55, + 10: 1.65, + 11: 1.77, + 12: 1.9, + 13: 2.06, + 14: 2.25, + 15: 2.48, + 16: 2.75, + 17: 3.09, + 18: 3.54, + 19: 4.13, + 20: 4.95, + 21: 6.19, + 22: 8.25, + 23: 12.38, + 24: 24.75, + }, + 2: { + 1: 1.08, + 2: 1.17, + 3: 1.29, + 4: 1.41, + 5: 1.56, + 6: 1.74, + 7: 1.94, + 8: 2.18, + 9: 2.48, + 10: 2.83, + 11: 3.26, + 12: 3.81, + 13: 4.5, + 14: 5.4, + 15: 6.6, + 16: 8.25, + 17: 10.61, + 18: 14.14, + 19: 19.8, + 20: 29.7, + 21: 49.5, + 22: 99, + 23: 297, + }, + 3: { + 1: 1.13, + 2: 1.29, + 3: 1.48, + 4: 1.71, + 5: 2, + 6: 2.35, + 7: 2.79, + 8: 3.35, + 9: 4.07, + 10: 5, + 11: 6.26, + 12: 7.96, + 13: 10.35, + 14: 13.8, + 15: 18.97, + 16: 27.11, + 17: 40.66, + 18: 65.06, + 19: 113.85, + 20: 227.7, + 21: 569.25, + 22: 2277, + }, + 4: { + 1: 1.18, + 2: 1.41, + 3: 1.71, + 4: 2.09, + 5: 2.58, + 6: 3.23, + 7: 4.09, + 8: 5.26, + 9: 6.88, + 10: 9.17, + 11: 12.51, + 12: 17.52, + 13: 25.3, + 14: 37.95, + 15: 59.64, + 16: 99.39, + 17: 178.91, + 18: 357.81, + 19: 834.9, + 20: 2504.7, + 21: 12523.5, + }, + 5: { + 1: 1.24, + 2: 1.56, + 3: 2, + 4: 2.58, + 5: 3.39, + 6: 4.52, + 7: 6.14, + 8: 8.5, + 9: 12.04, + 10: 17.52, + 11: 26.27, + 12: 40.87, + 13: 66.41, + 14: 113.85, + 15: 208.73, + 16: 417.45, + 17: 939.26, + 18: 2504.7, + 19: 8766.45, + 20: 52598.7, + }, + 6: { + 1: 1.3, + 2: 1.74, + 3: 2.35, + 4: 3.23, + 5: 4.52, + 6: 6.46, + 7: 9.44, + 8: 14.17, + 9: 21.89, + 10: 35.03, + 11: 58.38, + 12: 102.17, + 13: 189.75, + 14: 379.5, + 15: 834.9, + 16: 2087.25, + 17: 6261.75, + 18: 25047, + 19: 175329, + }, + 7: { + 1: 1.38, + 2: 1.94, + 3: 2.79, + 4: 4.09, + 5: 6.14, + 6: 9.44, + 7: 14.95, + 8: 24.47, + 9: 41.6, + 10: 73.95, + 11: 138.66, + 12: 277.33, + 13: 600.88, + 14: 1442.1, + 15: 3965.78, + 16: 13219.25, + 17: 59486.63, + 18: 475893, + }, + 8: { + 1: 1.46, + 2: 2.18, + 3: 3.35, + 4: 5.26, + 5: 8.5, + 6: 14.17, + 7: 24.47, + 8: 44.05, + 9: 83.2, + 10: 166.4, + 11: 356.56, + 12: 831.98, + 13: 2163.15, + 14: 6489.45, + 15: 23794.65, + 16: 118973.25, + 17: 1070759.25, + }, + 9: { + 1: 1.55, + 2: 2.47, + 3: 4.07, + 4: 6.88, + 5: 12.04, + 6: 21.89, + 7: 41.6, + 8: 83.2, + 9: 176.8, + 10: 404.1, + 11: 1010.26, + 12: 2828.73, + 13: 9193.39, + 14: 36773.55, + 15: 202254.52, + 16: 2022545.25, + }, + 10: { + 1: 1.65, + 2: 2.83, + 3: 5, + 4: 9.17, + 5: 17.52, + 6: 35.03, + 7: 73.95, + 8: 166.4, + 9: 404.1, + 10: 1077.61, + 11: 3232.84, + 12: 11314.94, + 13: 49031.4, + 14: 294188.4, + 15: 3236072.4, + }, + 11: { + 1: 1.77, + 2: 3.26, + 3: 6.26, + 4: 12.51, + 5: 26.27, + 6: 58.38, + 7: 138.66, + 8: 356.56, + 9: 1010.26, + 10: 3232.84, + 11: 12123.15, + 12: 56574.69, + 13: 367735.5, + 14: 4412826, + }, + 12: { + 1: 1.9, + 2: 3.81, + 3: 7.96, + 4: 17.52, + 5: 40.87, + 6: 102.17, + 7: 277.33, + 8: 831.98, + 9: 2828.73, + 10: 11314.94, + 11: 56574.69, + 12: 396022.85, + 13: 5148297, + }, + 13: { + 1: 2.06, + 2: 4.5, + 3: 10.35, + 4: 25.3, + 5: 66.41, + 6: 189.75, + 7: 600.88, + 8: 2163.15, + 9: 9193.39, + 10: 49031.4, + 11: 367735.5, + 12: 5148297, + }, + 14: { + 1: 2.25, + 2: 5.4, + 3: 13.8, + 4: 37.95, + 5: 113.85, + 6: 379.5, + 7: 1442.1, + 8: 6489.45, + 9: 36773.55, + 10: 294188.4, + 11: 4412826, + }, + 15: { + 1: 2.47, + 2: 6.6, + 3: 18.97, + 4: 59.64, + 5: 208.73, + 6: 834.9, + 7: 3965.78, + 8: 23794.65, + 9: 202254.53, + 10: 3236072.4, + }, + 16: { + 1: 2.75, + 2: 8.25, + 3: 27.11, + 4: 99.39, + 5: 417.45, + 6: 2087.25, + 7: 13219.25, + 8: 118973.25, + 9: 2022545.25, + }, + 17: { + 1: 3.09, + 2: 10.61, + 3: 40.66, + 4: 178.91, + 5: 939.26, + 6: 6261.75, + 7: 59486.63, + 8: 1070759.25, + }, + 18: { + 1: 3.54, + 2: 14.14, + 3: 65.06, + 4: 357.81, + 5: 2504.7, + 6: 25047, + 7: 475893, + }, + 19: { + 1: 4.13, + 2: 19.8, + 3: 113.85, + 4: 834.9, + 5: 8766.45, + 6: 175329, + }, + 20: { 1: 4.95, 2: 29.7, 3: 227.7, 4: 2504.7, 5: 52598.7 }, + 21: { 1: 6.19, 2: 49.5, 3: 569.25, 4: 12523.5 }, + 22: { 1: 8.25, 2: 99, 3: 2277 }, + 23: { 1: 12.38, 2: 297 }, + 24: { 1: 24.75 }, +}; diff --git a/apps/api/src/features/games/mines/mines.controller.ts b/apps/api/src/features/games/mines/mines.controller.ts new file mode 100644 index 0000000..aea482e --- /dev/null +++ b/apps/api/src/features/games/mines/mines.controller.ts @@ -0,0 +1,155 @@ +import type { Request, Response } from 'express'; +import { MinesBetSchema } from '@repo/common/game-utils/mines/validations.js'; +import type { User } from '@prisma/client'; +import { ApiResponse } from '@repo/common/types'; +import { StatusCodes } from 'http-status-codes'; +import type { + MinesGameOverResponse, + MinesHiddenState, + MinesPlayRoundResponse, +} from '@repo/common/game-utils/mines/types.js'; +import { userManager } from '../../user/user.service'; +import { minesManager } from './mines.service'; + +interface PlayRoundRequestBody { + selectedTileIndex: number; + id: string; +} + +export const startGame = async ( + req: Request, + res: Response | { message: string }> +) => { + const validationResult = MinesBetSchema.safeParse(req.body); + + if (!validationResult.success) { + return res + .status(400) + .json( + new ApiResponse( + StatusCodes.BAD_REQUEST, + {}, + validationResult.error.message + ) + ); + } + + const { betAmount, minesCount } = validationResult.data; + + const userInstance = await userManager.getUser((req.user as User).id); + const user = userInstance.getUser(); + + const betAmountInCents = Math.round(betAmount * 100); + const userBalanceInCents = userInstance.getBalanceAsNumber(); + + if (userBalanceInCents < betAmountInCents) { + return res + .status(400) + .json( + new ApiResponse(StatusCodes.BAD_REQUEST, {}, 'Insufficient balance') + ); + } + + const game = await minesManager.createGame({ + betAmount: betAmountInCents, + minesCount, + userId: user.id, + }); + + const bet = game.getBet(); + + res.status(StatusCodes.OK).json( + new ApiResponse(StatusCodes.OK, { + id: bet.id, + active: true, + state: { mines: null, minesCount, rounds: [] }, + betAmount: betAmountInCents, + }) + ); +}; + +export const playRound = async ( + req: Request, + res: Response< + | ApiResponse + | { message: string } + > +) => { + const userInstance = await userManager.getUser((req.user as User).id); + const user = userInstance.getUser(); + + const { selectedTileIndex } = req.body as PlayRoundRequestBody; + const game = await minesManager.getGame(user.id); + + if (!game?.getBet().active) { + return res + .status(StatusCodes.BAD_REQUEST) + .json(new ApiResponse(StatusCodes.BAD_REQUEST, {}, 'Game not found')); + } + + const gameState = await game.playRound(selectedTileIndex); + if (!gameState.active) { + minesManager.deleteGame(user.id); + } + return res + .status(StatusCodes.OK) + .json(new ApiResponse(StatusCodes.OK, gameState)); +}; + +export const cashOut = async ( + req: Request, + res: Response< + ApiResponse | { message: string } + > +) => { + const userId = (req.user as User).id; + const game = await minesManager.getGame(userId); + if (!game?.getBet().active) { + return res + .status(StatusCodes.BAD_REQUEST) + .json(new ApiResponse(StatusCodes.BAD_REQUEST, {}, 'Game not found')); + } + + const gameState = await game.cashOut(userId); + minesManager.deleteGame(userId); + return res + .status(StatusCodes.OK) + .json(new ApiResponse(StatusCodes.OK, gameState)); +}; + +export const getActiveGame = async ( + req: Request, + res: Response< + ApiResponse | { message: string } + > +) => { + const userId = (req.user as User).id; + const game = await minesManager.getGame(userId); + + if (!game) { + return res + .status(StatusCodes.NOT_FOUND) + .json(new ApiResponse(StatusCodes.NOT_FOUND, {}, 'Game not found')); + } + + const activeBet = game.getBet(); + + if (!activeBet.active || !activeBet.state) { + return res + .status(StatusCodes.NOT_FOUND) + .json(new ApiResponse(StatusCodes.NOT_FOUND, {}, 'Game not found')); + } + + return res.status(StatusCodes.OK).json( + new ApiResponse(StatusCodes.OK, { + id: activeBet.id, + active: activeBet.active, + state: { + mines: null, + rounds: game.getRounds(), + minesCount: (activeBet.state as unknown as MinesHiddenState).minesCount, + }, + betAmount: activeBet.betAmount / 100, + }) + ); +}; diff --git a/apps/api/src/features/games/mines/mines.router.ts b/apps/api/src/features/games/mines/mines.router.ts new file mode 100644 index 0000000..d95850c --- /dev/null +++ b/apps/api/src/features/games/mines/mines.router.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { isAuthenticated } from '../../../middlewares/auth.middleware'; +import { + cashOut, + getActiveGame, + playRound, + startGame, +} from './mines.controller'; + +const minesRouter: Router = Router(); + +minesRouter.post('/start', isAuthenticated, startGame); +minesRouter.post('/play-round', isAuthenticated, playRound); +minesRouter.post('/cash-out', isAuthenticated, cashOut); +minesRouter.get('/active', isAuthenticated, getActiveGame); + +export default minesRouter; diff --git a/apps/api/src/features/games/mines/mines.service.ts b/apps/api/src/features/games/mines/mines.service.ts new file mode 100644 index 0000000..4d6051b --- /dev/null +++ b/apps/api/src/features/games/mines/mines.service.ts @@ -0,0 +1,205 @@ +import { NO_OF_TILES } from '@repo/common/game-utils/mines/constants.js'; +import type { + MinesBet, + MinesGameOverResponse, + MinesHiddenState, + MinesPlayRoundResponse, + MinesRevealedState, +} from '@repo/common/game-utils/mines/types.js'; +import db from '@repo/db'; +import type { Bet } from '@prisma/client'; +import { + convertFloatsToGameEvents, + calculateMines, +} from '@repo/common/game-utils/mines/utils.js'; +import { userManager } from '../../user/user.service'; +import { payouts } from './mines.constant'; + +class MinesManager { + private static instance: MinesManager | undefined; + private games: Map; + + private constructor() { + this.games = new Map(); + } + + static getInstance() { + if (!MinesManager.instance) { + MinesManager.instance = new MinesManager(); + } + return MinesManager.instance; + } + + async getGame(userId: string) { + if (!this.games.has(userId)) { + const bet = await db.bet.findFirst({ + where: { userId, active: true }, + }); + + if (!bet) { + return null; + } + const game = new Mines(bet); + this.games.set(userId, game); + } + return this.games.get(userId); + } + + async createGame({ + minesCount, + betAmount, + userId, + }: MinesBet & { userId: string }) { + const userInstance = await userManager.getUser(userId); + const floats = userInstance.generateFloats(NO_OF_TILES - 1); + const gameEvents = convertFloatsToGameEvents(floats, NO_OF_TILES); + const mines = calculateMines(gameEvents, minesCount); + const createdBet = await db.$transaction(async tx => { + const bet = await tx.bet.create({ + data: { + active: true, + betAmount, + betNonce: userInstance.getNonce(), + game: 'mines', + provablyFairStateId: userInstance.getProvablyFairStateId(), + state: { + mines, + minesCount, + rounds: [], + }, + userId: userInstance.getUser().id, + payoutAmount: 0, + }, + }); + await userInstance.updateNonce(tx); + return bet; + }); + const game = new Mines(createdBet); + this.games.set(createdBet.userId, game); + return game; + } + + deleteGame(userId: string) { + this.games.delete(userId); + } +} + +class Mines { + private bet: Bet; + private rounds: { selectedTileIndex: number; payoutMultiplier: number }[] = + []; + private selectedTiles: number[] = []; + constructor(bet: Bet) { + this.bet = bet; + } + + getBet() { + return this.bet; + } + + getRounds() { + return this.rounds; + } + + async playRound( + selectedTileIndex: number + ): Promise { + if (this.selectedTiles.includes(selectedTileIndex)) { + throw new Error('Tile already selected'); + } else { + this.selectedTiles.push(selectedTileIndex); + } + if (this.rounds.length === NO_OF_TILES - 1) { + throw new Error('Game over'); + } + if (!this.bet.state) { + throw new Error('Game state not found'); + } + const { mines, minesCount } = this.bet.state as unknown as + | MinesHiddenState + | MinesRevealedState; + if (!mines) { + throw new Error('Game not started'); + } + if (mines.includes(selectedTileIndex)) { + this.rounds.push({ selectedTileIndex, payoutMultiplier: 0 }); + return this.getGameOverState(this.bet.userId); + } + const gemsCount = this.rounds.length + 1; + + const payoutMultiplier = payouts[gemsCount][minesCount]; + + this.rounds.push({ selectedTileIndex, payoutMultiplier }); + await db.bet.update({ + where: { id: this.bet.id, active: true }, + data: { + state: { + ...(this.bet.state as unknown as MinesHiddenState), + rounds: this.rounds, + }, + }, + }); + return { + id: this.bet.id, + active: true, + state: { + rounds: this.rounds, + mines: null, + minesCount, + }, + betAmount: this.bet.betAmount / 100, + }; + } + + async cashOut(userId: string) { + if (this.rounds.length === 0) { + throw new Error('Game not started'); + } + return this.getGameOverState(userId); + } + + private async getGameOverState( + userId: string + ): Promise { + const userInstance = await userManager.getUser(userId); + const payoutMultiplier = this.rounds.at(-1)?.payoutMultiplier || 0; + const payoutAmount = payoutMultiplier * this.bet.betAmount; + const balanceChangeInCents = payoutAmount - this.bet.betAmount; + + const userBalanceInCents = userInstance.getBalanceAsNumber(); + const newBalance = (userBalanceInCents + balanceChangeInCents).toString(); + + const balance = await db.$transaction(async tx => { + await tx.bet.update({ + where: { id: this.bet.id }, + data: { + payoutAmount, + active: false, + state: this.bet.state || {}, + }, + }); + + const userWithNewBalance = await tx.user.update({ + where: { id: userId }, + data: { + balance: newBalance, + }, + }); + return userWithNewBalance.balance; + }); + userInstance.setBalance(balance); + return { + id: this.bet.id, + state: { + ...(this.bet.state as unknown as MinesRevealedState), + rounds: this.rounds, + }, + payoutMultiplier, + payout: Number((payoutAmount / 100).toFixed(2)), + balance: Number((parseInt(balance, 10) / 100).toFixed(2)), + active: false, + }; + } +} + +export const minesManager = MinesManager.getInstance(); diff --git a/apps/api/src/features/games/plinkoo/plinkoo.constants.ts b/apps/api/src/features/games/plinkoo/plinkoo.constants.ts new file mode 100644 index 0000000..8e8e1b4 --- /dev/null +++ b/apps/api/src/features/games/plinkoo/plinkoo.constants.ts @@ -0,0 +1,189 @@ +export const TOTAL_DROPS = 16; + +export const MULTIPLIERS: Record = { + 0: 16, + 1: 9, + 2: 2, + 3: 1.4, + 4: 1.4, + 5: 1.2, + 6: 1.1, + 7: 1, + 8: 0.5, + 9: 1, + 10: 1.1, + 11: 1.2, + 12: 1.4, + 13: 1.4, + 14: 2, + 15: 9, + 16: 16, +}; + +export const OUTCOMES: Record = { + '0': [], + '1': [3964963.452981615, 3910113.3998412564], + '2': [ + 3980805.7004139693, 3945617.6504109767, 4027628.395823398, + 3902115.8620758583, 3938709.5467746584, + ], + '3': [ + 3975554.824601942, 3965805.769610554, 3909279.443666201, 3940971.550465178, + 3909606.717374134, 3915484.1741136736, 3977018.430328505, + 3979167.5933461944, 3995981.0273005674, 3974177.78840204, + ], + '4': [ + 3943174.7607756723, 3992961.0886867167, 3914511.2798374896, + 3950487.300703086, 3973378.3900412438, 4012888.985549594, + 4040961.8767680754, 4066503.3857407006, 3944573.7194061875, + 3979876.769324002, 4042712.772834604, 4032991.0303322095, + 4046340.7919081766, 3912597.9665436875, 4068852.495940549, + 4064879.257329362, 3996796.04239161, 4045062.2783860737, 3964680.919169739, + ], + '5': [ + 3953045.1447091424, 3947374.62976226, 3924082.6101653073, 3919085.269354398, + 3902650.4008744615, 3934968.1593932374, 4044126.7590222214, + 3928499.8807134246, 3913801.9247018984, 3909595.4432100505, + 4082827.827013994, 3979739.108665962, 4077651.317785833, 4008030.8883127486, + 3950951.6007580766, 3992039.9053288833, 4021810.0928285993, + 4052650.560434505, 3994806.267259329, 3959327.3735489477, + 3940455.7641962855, 3998822.2807239015, 3998803.9335444313, + 4068193.3913483596, 3938798.911585438, + ], + '6': [ + 4065643.7049927213, 3936841.961313155, 3948472.8991447487, + 4004510.5975928125, 3933695.6888747592, 4011296.1958215656, + 4093232.84383817, 3945658.6170622837, 4063199.5117669366, 4037864.799653558, + 3931477.3517858014, 4091381.513010509, 4000895.053297006, + 4042867.6535872207, 4090947.938511616, 3989468.333758437, 3943335.764879169, + 3947278.536321405, 4022304.817103859, 3902177.8466275427, 3925270.959381573, + 3955253.4540312397, 3986641.0060988157, 3927696.2396482667, + 4064571.150949869, 3991167.946685552, 3973041.308793569, 3987377.180906899, + 3917262.667253392, 4002606.795366179, 4033596.992526079, 3901372.366183016, + 4015207.583244224, 3955421.290959922, 3952223.0425123484, + 3941774.4498685915, 3977289.3718391117, 4024943.3014183883, + 4024885.5052148327, 4016596.7449097126, 3910164.1864616796, + 4023400.498352244, 3981421.8628830933, 3913377.3496230906, + 4045958.9425667236, 4071139.892029292, 4019862.922309672, + 4027992.2300945413, 4030455.1701347437, 4060673.10227606, 3996564.062673036, + 4009801.4052053, 4007734.404953163, 4046612.754675019, 3944956.9979153597, + 3977382.889196781, 3906636.5132748624, 4080470.0674178666, + 3996210.4877184015, 3956216.294023866, 3940040.183231992, + ], + '7': [ + 3926739.9104774813, 4091374.44234272, 4061919.9903071183, + 3976066.7555194413, 3948801.1936986246, 4043233.7830772344, + 4010011.7658794387, 3936431.4108806592, 3942776.8649452417, + 3909995.011479453, 4012272.43979473, 3989907.069429411, 3996182.4336681785, + 4078644.79693604, 4081624.0834239917, 4025044.731614778, 4033602.5381773794, + 3913189.826642105, 3910500.674962151, 4055296.6588616692, + 4005574.8641647273, 4079800.3518520766, 4092763.5236495608, + 3952185.4910905147, 3945510.495018459, 3920891.8818843197, + 3997101.789672143, 3991974.822516503, 3949265.4371072412, + 3933412.4749754136, 3933181.8312838264, 4063875.6616431624, + 3998206.7252218956, 3959006.1987530286, 3924067.917601976, + 3902914.4459602935, 3905347.098696195, 4000831.565288375, 3944915.3251241, + 3930343.481158048, 4025858.616981573, 4026496.026592473, 3948116.019901921, + 4067143.737297127, 3995156.000931595, 3905006.3301882823, + 4035783.4852589793, 3956461.6106608217, 4032886.6912715673, + 3913146.10237042, 3930772.085213345, 3984887.619042549, 4053031.0321973227, + 3913395.137097174, 3993579.678508536, 3932427.236196532, 3984279.0886106077, + ], + '8': [ + 4099062.75134143, 4085894.4181278455, 3991123.0115790954, + 3973053.5827605873, 3968190.564301313, 3925604.5066868863, + 3933898.7590061547, 4089919.7991958153, 4076997.5225973814, + 3957630.60529322, 3948999.35996541, 3963938.9455971997, 4044805.7991237757, + 3905133.2109927135, 4074463.6876271376, 3939301.0655442886, + 4040571.320635691, 4020510.19979044, 3959835.4618981928, 4037241.67248416, + 4043105.87901907, 3912654.2409310103, 3929773.262095125, 3950802.527033251, + 4068582.4605300324, 3946792.6177569656, 4078475.9982660934, + 3972024.763383927, 3947150.677862883, 3963410.9779685168, 3999134.851845996, + 3909374.1117644133, 3942761.896008833, 4071253.4107468165, 4050534.50171971, + 3988521.4618817912, 3929940.089627246, 4029305.1056314665, + 4087943.221841722, 3910909.3079385986, 4046944.0552393594, + 4006944.159180551, 4014707.657017377, 3925473.574267122, 4012158.905329344, + 4042197.149473071, 3998434.6078570196, 4047267.2747256896, + 3964753.3725316986, 3955821.0222197613, 3973475.662585886, + 3917189.0280630635, 4027132.7848505056, 3905368.7668914935, + 3936654.62186107, 4092566.3229272505, 4026541.0685970024, + 4038770.6420815475, 4067262.4257867294, 4050430.5327158393, + 3980149.8069138955, 4052184.5678737606, 3942299.598280835, + 4079754.687607573, 4021112.5651541506, 3961023.3381184433, + 3937025.1424917267, 3964607.486702018, 4001319.0133674755, + 3941648.5232227165, 4030587.9685114417, 4044067.1579758436, + 4058158.522928313, + ], + '9': [ + 3911530.315770063, 4024711.492410591, 3967652.4297853387, + 4098886.3793751886, 4026117.0283389515, 4045045.4095477182, + 4034571.220507859, 4088809.303306565, 3900806.968890352, 3913166.9251142726, + 4059594.3600833854, 3945137.694311404, 3902668.8160601873, + 4054646.2889849013, 4053898.6542759663, 3959251.11275926, 3963475.882565954, + 3967968.9310842347, 4075078.929914972, 4035117.4533019722, + 4047608.2592268144, 3913024.5010530455, 4081362.0390194473, + 4098538.7144543654, 4049336.7774994993, 4056844.5727342237, + 3917845.6810319433, 4098332.1779752634, 3979547.7686487637, + 4026747.155594485, 3944692.803167993, 3960649.105237204, 4081040.2295870385, + 4005698.9658651184, 4074183.694152899, 3976184.3586868607, + 4007157.5084493076, 3918927.3398626954, 3918166.0285542854, + 3953868.3374998523, 3963648.6249533077, 4065036.1837552087, + 3964230.698479104, 3992799.530672317, 3931113.922813188, 4082916.6661583954, + 3919236.111874976, 4012743.1541231154, 3900406.2441578982, + 4031396.764516756, 4088712.2834741194, 3921570.4946371615, 4077416.64169384, + 3962807.6000533635, + ], + '10': [ + 4069582.648305392, 3966300.3577461895, 4047184.7847023425, + 3962656.256238744, 3934682.0223851865, 4089620.291559703, 3996605.065672608, + 3921656.567101851, 3950930.30704122, 4052733.606190915, 4046762.051641918, + 3912718.72211605, 3942094.6698735086, 4017504.735499972, 4016206.1612997893, + 4060896.040328729, 4077224.686824909, 3988932.185505723, 4016550.502499315, + 3959104.134236025, 3903531.023685199, 3939907.5585800377, 3969464.753065079, + 4036549.7059165714, 3938844.715578784, 3985594.4268763512, + 4011615.276676018, 3949739.058361909, 4064041.8926257566, 4004767.498301687, + 3996411.8026064364, 4035064.3182208547, 3988008.7378418343, + 4015638.96642283, 3967068.722994021, 4082965.2856357233, 3951302.134707721, + 3948101.1830631103, 3978745.8509503608, 4068638.265329366, + 4018433.726155858, 4032765.523475676, + ], + '11': [ + 4055462.593704495, 4027576.362231998, 4011290.7395424685, + 4034848.6574270525, 4064298.598636101, 3997022.919190929, 4053625.932623065, + 4064234.3514714935, 4075348.9710445153, 4060118.5348266517, + 4065992.932112665, 4063162.143518177, 4060798.1858924176, 3956764.654354398, + 3912916.1668887464, 4018282.0763658765, 4065575.3280486814, + 3967348.3916016137, 4034992.477051428, 4069123.2018048204, + 3939281.4172981237, 4022103.802712647, 4083993.320300048, 4034478.871034405, + 4068844.513451607, 4097187.535489012, 3981130.4047553614, + 4068312.6406908804, 4050921.0879167155, 4048297.277514315, + 3953878.475004285, 3998627.3710734197, + ], + '12': [ + 4007152.5182738686, 4014664.8542149696, 4095619.5802802853, + 4018084.7270321106, 4072050.3744347296, 4026256.723716898, + 4095827.9573665825, 4023631.9896559394, 4046751.9125588783, + 3973758.674124694, 4081927.075527175, 3922485.387310559, 4001549.2805312183, + 4050417.849670596, 3987607.4531957353, 4060206.9664999805, + 4080316.8473846694, 4030455.1532406537, 4087714.965906726, + 4028165.0792610054, 4032588.5261474997, 3980546.468460318, + 4090408.033691761, 3990019.103297975, 4088755.998466496, 4092162.22327816, + 4029036.6583707742, 4055066.505591603, 4081998.821392285, 4079550.553314541, + ], + '13': [ + 3905319.849889843, 4054719.0660902266, 4055596.4319745116, + 3992648.989962779, 3924972.5941170114, 4095167.7814041013, + 3912740.1944122575, 4024882.9438952096, 4023171.3988155797, + 4059892.954049364, 4068510.96886605, 4093838.431690223, 4070524.1327491063, + ], + '14': [ + 4092261.8249403643, 3956304.3865069468, 4069053.2302732924, + 4038890.8473817194, + ], + '15': [ + 4013891.110502415, 3977489.9532032954, 4044335.989753631, + 4066199.8081775964, + ], + '16': [3979706.1687804307, 4024156.037977316], + '17': [], +}; diff --git a/apps/api/src/features/games/plinkoo/plinkoo.controller.ts b/apps/api/src/features/games/plinkoo/plinkoo.controller.ts new file mode 100644 index 0000000..859df81 --- /dev/null +++ b/apps/api/src/features/games/plinkoo/plinkoo.controller.ts @@ -0,0 +1,10 @@ +import type { Request, Response } from 'express'; +import { calculateOutcome } from './plinkoo.service'; + +export const getOutcome = (req: Request, res: Response): void => { + const { clientSeed = 'P7xjSv-1ff' } = req.body as { + clientSeed: string; + }; + const result = calculateOutcome(clientSeed); + res.send(result); +}; diff --git a/apps/api/src/features/games/plinkoo/plinkoo.router.ts b/apps/api/src/features/games/plinkoo/plinkoo.router.ts new file mode 100644 index 0000000..9a18da8 --- /dev/null +++ b/apps/api/src/features/games/plinkoo/plinkoo.router.ts @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import { getOutcome } from './plinkoo.controller'; + +const plinkooRouter: Router = Router(); + +plinkooRouter.post('/outcome', getOutcome); + +export default plinkooRouter; diff --git a/apps/api/src/features/games/plinkoo/plinkoo.service.ts b/apps/api/src/features/games/plinkoo/plinkoo.service.ts new file mode 100644 index 0000000..42ca2af --- /dev/null +++ b/apps/api/src/features/games/plinkoo/plinkoo.service.ts @@ -0,0 +1,32 @@ +// import { rng } from '../../user/user.service'; +import { OUTCOMES, MULTIPLIERS } from './plinkoo.constants'; + +type TPattern = ('L' | 'R')[]; + +const DIRECTIONS: TPattern = ['L', 'R']; + +export const calculateOutcome = () => { + let outcome = 0; + const pattern: TPattern = []; + // const floats = rng.generateFloats({ clientSeed, count: TOTAL_DROPS }); + const floats = [2]; + floats.forEach(float => { + const direction = DIRECTIONS[Math.floor(float * 2)]; // 0 or 1 -> L or R + pattern.push(direction); + if (direction === 'R') { + outcome++; + } + }); + + const multiplier = MULTIPLIERS[outcome]; + const possiblieOutcomes = OUTCOMES[outcome]; + + return { + point: + possiblieOutcomes[ + Math.floor(Math.random() * possiblieOutcomes.length || 0) + ], + multiplier, + pattern, + }; +}; diff --git a/apps/api/src/features/games/roulette/roulette.constants.ts b/apps/api/src/features/games/roulette/roulette.constants.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/src/features/games/roulette/roulette.controller.ts b/apps/api/src/features/games/roulette/roulette.controller.ts new file mode 100644 index 0000000..ea9df9b --- /dev/null +++ b/apps/api/src/features/games/roulette/roulette.controller.ts @@ -0,0 +1,102 @@ +import db from '@repo/db'; +import type { Request, Response } from 'express'; +import type { RoulettePlaceBetResponse } from '@repo/common/game-utils/roulette/index.js'; +import { + BetsSchema, + validateBets, +} from '@repo/common/game-utils/roulette/index.js'; +import { type User } from '@prisma/client'; +import { StatusCodes } from 'http-status-codes'; +import { ApiResponse } from '@repo/common/types'; +import { sum } from 'lodash'; +import { BadRequestError } from '../../../errors'; +import { userManager } from '../../user/user.service'; +import { calculatePayout, spinWheel } from './roulette.service'; + +export const placeBetAndSpin = async ( + request: Request, + response: Response> +): Promise => { + const validationResult = BetsSchema.safeParse(request.body); + + if (!validationResult.success) { + throw new BadRequestError('Invalid request for bets'); + } + + const { bets } = validationResult.data; + + const validBets = validateBets(bets); + + if (validBets.length === 0) { + throw new BadRequestError('No valid bets placed'); + } + + const userInstance = await userManager.getUser((request.user as User).id); + const user = userInstance.getUser(); + + const totalBetAmountInCents = Math.round( + sum(validBets.map(bet => bet.amount)) * 100 + ); + + const userBalanceInCents = userInstance.getBalanceAsNumber(); + + if (userBalanceInCents < totalBetAmountInCents) { + throw new BadRequestError('Insufficient balance'); + } + + const winningNumber = await spinWheel(user.id); + + const payout = calculatePayout(validBets, winningNumber); + + const gameState = { + bets: validBets, + winningNumber: String(winningNumber), + }; + + const payoutInCents = Math.round(payout * 100); + + const balanceChangeInCents = payoutInCents - totalBetAmountInCents; + + const newBalance = (userBalanceInCents + balanceChangeInCents).toString(); + + const { balance, id } = await db.$transaction(async tx => { + const bet = await tx.bet.create({ + data: { + active: false, + betAmount: totalBetAmountInCents, + betNonce: userInstance.getNonce(), + game: 'roulette', + payoutAmount: payoutInCents, + provablyFairStateId: userInstance.getProvablyFairStateId(), + state: gameState, + userId: user.id, + }, + }); + + await userInstance.updateNonce(tx); + + const userWithNewBalance = await tx.user.update({ + where: { id: user.id }, + data: { + balance: newBalance, + }, + }); + + return { + balance: userWithNewBalance.balance, + id: bet.id, + }; + }); + + userInstance.setBalance(balance); + + response.status(StatusCodes.OK).json( + new ApiResponse(StatusCodes.OK, { + id, + state: gameState, + payoutMultiplier: payoutInCents / totalBetAmountInCents, + payout: payoutInCents / 100, + balance: userInstance.getBalanceAsNumber() / 100, + }) + ); +}; diff --git a/apps/api/src/features/games/roulette/roulette.router.ts b/apps/api/src/features/games/roulette/roulette.router.ts new file mode 100644 index 0000000..4703b00 --- /dev/null +++ b/apps/api/src/features/games/roulette/roulette.router.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { isAuthenticated } from '../../../middlewares/auth.middleware'; +import { placeBetAndSpin } from './roulette.controller'; + +const rouletteRouter: Router = Router(); + +rouletteRouter.post('/place-bet', isAuthenticated, placeBetAndSpin); + +export default rouletteRouter; diff --git a/apps/api/src/features/games/roulette/roulette.service.ts b/apps/api/src/features/games/roulette/roulette.service.ts new file mode 100644 index 0000000..6d23a9d --- /dev/null +++ b/apps/api/src/features/games/roulette/roulette.service.ts @@ -0,0 +1,82 @@ +import { + blackNumbers, + redNumbers, + type RouletteBet, + RouletteBetTypes, +} from '@repo/common/game-utils/roulette/index.js'; +import { sum } from 'lodash'; +import { userManager } from '../../user/user.service'; +import { isNumberInRange } from '../../../utils/numbers'; + +const spinWheel = async (userId: string) => { + const user = await userManager.getUser(userId); + const [float] = user.generateFloats(1); + return Math.floor(float * 37); // Generates a number between 0 and 36 +}; + +const calculatePayout = (bets: RouletteBet[], winningNumber: number) => { + const payouts = bets.map(bet => { + switch (bet.betType) { + case RouletteBetTypes.STRAIGHT: + return bet.selection === winningNumber ? bet.amount * 36 : 0; + case RouletteBetTypes.SPLIT: + return bet.selection.includes(winningNumber) ? bet.amount * 18 : 0; + case RouletteBetTypes.STREET: + return bet.selection.includes(winningNumber) ? bet.amount * 12 : 0; + case RouletteBetTypes.CORNER: + return bet.selection.includes(winningNumber) ? bet.amount * 9 : 0; + case RouletteBetTypes.SIXLINE: + return bet.selection.includes(winningNumber) ? bet.amount * 6 : 0; + case RouletteBetTypes.DOZEN: + switch (bet.selection) { + case 1: + return isNumberInRange(winningNumber, 1, 12) ? bet.amount * 3 : 0; + case 2: + return isNumberInRange(winningNumber, 13, 24) ? bet.amount * 3 : 0; + case 3: + return isNumberInRange(winningNumber, 25, 36) ? bet.amount * 3 : 0; + default: + return 0; + } + case RouletteBetTypes.COLUMN: { + if (winningNumber === 0) return 0; + switch (bet.selection) { + case 1: + return winningNumber % 3 === 1 ? bet.amount * 3 : 0; + case 2: + return winningNumber % 3 === 2 ? bet.amount * 3 : 0; + case 3: + return winningNumber % 3 === 0 ? bet.amount * 3 : 0; + default: + return 0; + } + } + case RouletteBetTypes.BLACK: + return winningNumber !== 0 && + blackNumbers.includes(winningNumber.toString()) + ? bet.amount * 2 + : 0; + case RouletteBetTypes.RED: + return winningNumber !== 0 && + redNumbers.includes(winningNumber.toString()) + ? bet.amount * 2 + : 0; + case RouletteBetTypes.EVEN: + return winningNumber !== 0 && winningNumber % 2 === 0 + ? bet.amount * 2 + : 0; + case RouletteBetTypes.ODD: + return winningNumber % 2 === 1 ? bet.amount * 2 : 0; + case RouletteBetTypes.HIGH: + return isNumberInRange(winningNumber, 19, 36) ? bet.amount * 2 : 0; + case RouletteBetTypes.LOW: + return isNumberInRange(winningNumber, 1, 18) ? bet.amount * 2 : 0; + default: + return 0; + } + }); + + return sum(payouts); +}; + +export { spinWheel, calculatePayout }; diff --git a/apps/api/src/features/user/user.controller.ts b/apps/api/src/features/user/user.controller.ts new file mode 100644 index 0000000..08cab65 --- /dev/null +++ b/apps/api/src/features/user/user.controller.ts @@ -0,0 +1,84 @@ +import type { Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import type { User } from '@prisma/client'; +import { ApiResponse } from '@repo/common/types'; +import type { + PaginatedBetsResponse, + ProvablyFairStateResponse, +} from '@repo/common/types'; +import { BadRequestError } from '../../errors'; +import { userManager, getUserBets } from './user.service'; + +export const getBalance = async (req: Request, res: Response) => { + const userInstance = await userManager.getUser((req.user as User).id); + const balanceInCents = userInstance.getBalanceAsNumber(); + const balance = balanceInCents / 100; // Convert from cents to dollars + return res + .status(StatusCodes.OK) + .json(new ApiResponse(StatusCodes.OK, { balance })); +}; + +export const rotateSeed = async ( + req: Request, + res: Response> +) => { + const { clientSeed } = req.body as { clientSeed: string }; + if (!clientSeed) { + throw new BadRequestError('Client seed is required'); + } + const userInstance = await userManager.getUser((req.user as User).id); + const seed = await userInstance.rotateSeed(clientSeed); + return res.status(StatusCodes.OK).json(new ApiResponse(StatusCodes.OK, seed)); +}; + +export const getProvablyFairState = async ( + req: Request, + res: Response> +) => { + const userInstance = await userManager.getUser((req.user as User).id); + return res.status(StatusCodes.OK).json( + new ApiResponse(StatusCodes.OK, { + clientSeed: userInstance.getClientSeed(), + hashedServerSeed: userInstance.getHashedServerSeed(), + hashedNextServerSeed: userInstance.getHashedNextServerSeed(), + nonce: userInstance.getNonce(), + }) + ); +}; + +export const getRevealedServerSeed = async ( + req: Request, + res: Response> +) => { + const { hashedServerSeed } = req.params; + + if (!hashedServerSeed) { + throw new BadRequestError('Hashed server seed is required'); + } + + const userInstance = await userManager.getUser((req.user as User).id); + const serverSeed = + await userInstance.getRevealedServerSeedByHash(hashedServerSeed); + + return res + .status(StatusCodes.OK) + .json(new ApiResponse(StatusCodes.OK, { serverSeed })); +}; + +export const getUserBetHistory = async ( + req: Request, + res: Response> +) => { + const userId = (req.user as User).id; + + // Parse pagination parameters from query + const page = parseInt(req.query.page as string) || 1; + const pageSize = parseInt(req.query.pageSize as string) || 10; + + // Get paginated bets + const paginatedBets = await getUserBets({ userId, page, pageSize }); + + res + .status(StatusCodes.OK) + .json(new ApiResponse(StatusCodes.OK, paginatedBets)); +}; diff --git a/apps/api/src/features/user/user.router.ts b/apps/api/src/features/user/user.router.ts new file mode 100644 index 0000000..57afc3d --- /dev/null +++ b/apps/api/src/features/user/user.router.ts @@ -0,0 +1,23 @@ +import { Router } from 'express'; +import { isAuthenticated } from '../../middlewares/auth.middleware'; +import { + getBalance, + rotateSeed, + getProvablyFairState, + getRevealedServerSeed, + getUserBetHistory, +} from './user.controller'; + +const router: Router = Router(); + +router.get('/balance', isAuthenticated, getBalance); +router.post('/rotate-seeds', isAuthenticated, rotateSeed); +router.get('/provably-fair-state', isAuthenticated, getProvablyFairState); +router.get( + '/unhash-server-seed/:hashedServerSeed', + isAuthenticated, + getRevealedServerSeed +); +router.get('/bets', isAuthenticated, getUserBetHistory); + +export default router; diff --git a/apps/api/src/features/user/user.service.ts b/apps/api/src/features/user/user.service.ts new file mode 100644 index 0000000..c8fb036 --- /dev/null +++ b/apps/api/src/features/user/user.service.ts @@ -0,0 +1,265 @@ +import type { Prisma, ProvablyFairState, User } from '@prisma/client'; +import db from '@repo/db'; +import type { ProvablyFairStateResponse } from '@repo/common/types'; +import { + getGeneratedFloats, + getHashedSeed, + getHmacSeed, +} from '@repo/common/game-utils/provably-fair/utils.js'; +import { BadRequestError } from '../../errors'; +import { generateClientSeed, generateServerSeed } from './user.utils'; + +export class UserInstance { + constructor( + private user: User, + private provablyFairState: ProvablyFairState + ) {} + + setBalance(amount: string) { + // Ensure the balance is stored as a string + this.user.balance = amount; + } + + getUser() { + return this.user; + } + + async rotateSeed(clientSeed: string): Promise { + const newServerSeed = this.generateNextServerSeed(); + const hashedServerSeed = getHashedSeed(newServerSeed); + + const result = await db.$transaction(async tx => { + // Mark current seed as revealed + await tx.provablyFairState.update({ + where: { id: this.provablyFairState.id }, + data: { revealed: true }, + }); + + // Create new seeds + const updated = await tx.provablyFairState.create({ + data: { + serverSeed: newServerSeed, + hashedServerSeed, + clientSeed, + revealed: false, + nonce: 0, + userId: this.user.id, + }, + }); + + // Update instance state + this.provablyFairState = updated; + + return { + clientSeed, + hashedServerSeed: this.getHashedServerSeed(), + hashedNextServerSeed: this.getHashedNextServerSeed(), + nonce: updated.nonce, + }; + }); + + return result; + } + + getBalance(): string { + return this.user.balance; + } + + // Convert balance to number for calculations when needed + getBalanceAsNumber(): number { + return parseInt(this.user.balance, 10); + } + + async updateNonce(tx: Prisma.TransactionClient) { + await tx.provablyFairState.update({ + where: { id: this.provablyFairState.id }, + data: { nonce: this.provablyFairState.nonce }, + }); + this.provablyFairState.nonce += 1; + } + + getProvablyFairStateId() { + return this.provablyFairState.id; + } + + getServerSeed() { + return this.provablyFairState.serverSeed; + } + + getClientSeed() { + return this.provablyFairState.clientSeed; + } + + getNonce() { + return this.provablyFairState.nonce; + } + + getHashedServerSeed() { + return getHashedSeed(this.provablyFairState.serverSeed); + } + + getHashedNextServerSeed() { + const nextServerSeed = this.generateNextServerSeed(); + return getHashedSeed(nextServerSeed); + } + + private generateNextServerSeed(): string { + return getHmacSeed(this.provablyFairState.serverSeed, 'next-seed'); + } + + generateFloats(count: number): number[] { + return getGeneratedFloats({ + count, + seed: this.provablyFairState.serverSeed, + message: `${this.provablyFairState.clientSeed}:${this.provablyFairState.nonce}`, + }); + } + + // Function to get a revealed server seed by its hash + async getRevealedServerSeedByHash( + hashedServerSeed: string + ): Promise { + const revealedState = await db.provablyFairState.findFirst({ + where: { + hashedServerSeed, + revealed: true, + userId: this.user.id, + }, + }); + + if (!revealedState) { + return null; + } + + return revealedState.serverSeed; + } +} + +class UserManager { + private static instance: UserManager | undefined; + private users = new Map(); + + static getInstance() { + if (!UserManager.instance) { + UserManager.instance = new UserManager(); + } + return UserManager.instance; + } + + async getUser(userId: string): Promise { + if (!this.users.has(userId)) { + const user = await db.user.findUnique({ + where: { id: userId }, + include: { + provablyFairStates: { + where: { + revealed: false, + }, + orderBy: { + createdAt: 'desc', + }, + take: 1, + }, + }, + }); + if (!user) { + throw new BadRequestError('User not found'); + } + if (!user.provablyFairStates[0]) { + // Create initial provably fair state if it doesn't exist + const provablyFairState = await db.provablyFairState.create({ + data: { + userId: user.id, + serverSeed: generateServerSeed(), + clientSeed: generateClientSeed(), + nonce: 0, + revealed: false, + }, + }); + user.provablyFairStates = [provablyFairState]; + } + this.users.set( + userId, + new UserInstance(user, user.provablyFairStates[0]) + ); + } + const user = this.users.get(userId); + if (!user) { + throw new BadRequestError('User not found in manager'); + } + return user; + } + + removeUser(userId: string) { + this.users.delete(userId); + } +} + +export const userManager = UserManager.getInstance(); + +export const getUserBets = async ({ + userId, + page = 1, + pageSize = 10, +}: { + userId: string; + page?: number; + pageSize?: number; +}) => { + // Ensure valid pagination parameters + const validPage = Math.max(1, page); + const validPageSize = Math.min(100, Math.max(1, pageSize)); + + // Get total count for pagination + const totalCount = await db.bet.count({ + where: { + userId, + }, + }); + + // Get paginated bets + const bets = await db.bet.findMany({ + where: { + userId, + }, + orderBy: { + createdAt: 'desc', + }, + include: { + user: { + select: { + id: true, + name: true, + }, + }, + }, + skip: (validPage - 1) * validPageSize, + take: validPageSize, + }); + + // Calculate pagination metadata + const totalPages = Math.ceil(totalCount / validPageSize); + const hasNextPage = validPage < totalPages; + const hasPreviousPage = validPage > 1; + + return { + bets: bets.map(bet => ({ + // Format betId as a 12-digit string with leading zeros + betId: bet.betId.toString().padStart(12, '0'), + game: bet.game, + date: bet.createdAt, + betAmount: bet.betAmount / 100, + payoutMultiplier: bet.payoutAmount / bet.betAmount, + payout: bet.payoutAmount / 100, + id: bet.id, + })), + pagination: { + page: validPage, + pageSize: validPageSize, + totalCount, + totalPages, + hasNextPage, + hasPreviousPage, + }, + }; +}; diff --git a/apps/api/src/features/user/user.utils.ts b/apps/api/src/features/user/user.utils.ts new file mode 100644 index 0000000..307a219 --- /dev/null +++ b/apps/api/src/features/user/user.utils.ts @@ -0,0 +1,9 @@ +import { randomBytes } from 'node:crypto'; + +export const generateClientSeed = () => { + return randomBytes(32).toString('hex'); +}; + +export const generateServerSeed = () => { + return randomBytes(32).toString('hex'); +}; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..bd33fe0 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,6 @@ +import { createServer } from './server'; + +const port = process.env.PORT || 5000; +const server = createServer(); + +server.listen(port); diff --git a/apps/api/src/middlewares/auth.middleware.ts b/apps/api/src/middlewares/auth.middleware.ts new file mode 100644 index 0000000..480965a --- /dev/null +++ b/apps/api/src/middlewares/auth.middleware.ts @@ -0,0 +1,16 @@ +import type { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; + +export const isAuthenticated = ( + req: Request, + res: Response, + next: NextFunction +): void => { + if (!req.isAuthenticated()) { + res.status(StatusCodes.UNAUTHORIZED).json({ + message: 'You must be logged in to access this resource', + }); + return; + } + next(); +}; diff --git a/apps/api/src/middlewares/auth.ts b/apps/api/src/middlewares/auth.ts new file mode 100644 index 0000000..6497324 --- /dev/null +++ b/apps/api/src/middlewares/auth.ts @@ -0,0 +1,16 @@ +import type { Response, NextFunction } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import type { AuthenticatedRequest } from '../types'; + +export const isAuthenticated = ( + req: AuthenticatedRequest, + res: Response, + next: NextFunction +) => { + if (!req.isAuthenticated()) { + return res.status(StatusCodes.UNAUTHORIZED).json({ + message: 'You must be logged in to access this resource', + }); + } + next(); +}; diff --git a/apps/api/src/middlewares/error-handler.ts b/apps/api/src/middlewares/error-handler.ts new file mode 100644 index 0000000..de812a1 --- /dev/null +++ b/apps/api/src/middlewares/error-handler.ts @@ -0,0 +1,26 @@ +import { ApiResponse } from '@repo/common/types'; +import type { Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; + +interface CustomError { + statusCode: number; + message: string; + data?: unknown; +} + +export const errorHandlerMiddleware = ( + err: Error | CustomError, + _: Request, + res: Response +) => { + const defaultError: CustomError = { + statusCode: + (err as CustomError).statusCode || StatusCodes.INTERNAL_SERVER_ERROR, + message: + (err as CustomError).message || 'Something went wrong, try again later', + }; + + return res + .status(defaultError.statusCode) + .json(new ApiResponse(defaultError.statusCode, {}, defaultError.message)); +}; diff --git a/apps/api/src/middlewares/not-found.ts b/apps/api/src/middlewares/not-found.ts new file mode 100644 index 0000000..b83a647 --- /dev/null +++ b/apps/api/src/middlewares/not-found.ts @@ -0,0 +1,11 @@ +import type { Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { ApiResponse } from '@repo/common/types'; + +const notFoundMiddleware = (_: Request, res: Response) => { + res + .status(StatusCodes.BAD_REQUEST) + .send(new ApiResponse(StatusCodes.BAD_REQUEST, {}, 'Route does not exist')); +}; + +export default notFoundMiddleware; diff --git a/apps/api/src/routes/index.ts b/apps/api/src/routes/index.ts new file mode 100644 index 0000000..a4dfc32 --- /dev/null +++ b/apps/api/src/routes/index.ts @@ -0,0 +1,5 @@ +import authRouter from '../features/auth/auth.router'; +import gameRouter from '../features/games/games.router'; +import userRouter from '../features/user/user.router'; + +export { authRouter, gameRouter, userRouter }; diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts new file mode 100644 index 0000000..4894c0c --- /dev/null +++ b/apps/api/src/server.ts @@ -0,0 +1,54 @@ +import 'express-async-errors'; +import 'dotenv/config'; + +import { json, urlencoded } from 'body-parser'; +import express, { type Express } from 'express'; +import morgan from 'morgan'; +import cors from 'cors'; +import session from 'express-session'; +import passport from 'passport'; +import { StatusCodes } from 'http-status-codes'; +import { authRouter, gameRouter, userRouter } from './routes'; +import './config/passport'; +import notFoundMiddleware from './middlewares/not-found'; +import { errorHandlerMiddleware } from './middlewares/error-handler'; + +export const createServer = (): Express => { + const app = express(); + app + .disable('x-powered-by') + .use(morgan('dev')) + .use(urlencoded({ extended: true })) + .use(json()) + .use( + cors({ + origin: process.env.CORS_ORIGIN || 'http://localhost:3000', // Use env variable with fallback + credentials: true, // Allow cookies and other credentials + }) + ) + .use( + session({ + secret: process.env.COOKIE_SECRET || 'secr3T', + cookie: { + secure: process.env.NODE_ENV === 'production' ? true : 'auto', + httpOnly: true, + maxAge: 2 * 24 * 60 * 60 * 1000, + }, + resave: false, + saveUninitialized: false, + }) + ) + .use(passport.initialize()) + .use(passport.session()) + .get('/health', (_, res) => { + return res.status(StatusCodes.OK).json({ ok: true }); + }) + .use('/api/v1/auth', authRouter) + .use('/api/v1/games', gameRouter) + .use('/api/v1/user', userRouter); + + app.use(notFoundMiddleware); + app.use(errorHandlerMiddleware); + + return app; +}; diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts new file mode 100644 index 0000000..09e7082 --- /dev/null +++ b/apps/api/src/types.ts @@ -0,0 +1,6 @@ +import type { Request } from 'express'; +import type { User } from '@prisma/client'; + +export interface AuthenticatedRequest extends Request { + user?: User; +} diff --git a/apps/api/src/types/index.ts b/apps/api/src/types/index.ts new file mode 100644 index 0000000..de35de4 --- /dev/null +++ b/apps/api/src/types/index.ts @@ -0,0 +1,6 @@ +import type { User } from '@prisma/client'; +import type { Request } from 'express'; + +export interface AuthenticatedRequest extends Request { + user: User | undefined; +} diff --git a/apps/api/src/utils/numbers.ts b/apps/api/src/utils/numbers.ts new file mode 100644 index 0000000..86193f0 --- /dev/null +++ b/apps/api/src/utils/numbers.ts @@ -0,0 +1,13 @@ +/** + * Checks if a number is within a specified range. + * + * @param number - The number to check. + * @param min - The minimum value of the range. + * @param max - The maximum value of the range. + * @returns - Returns true if the number is within the range, otherwise false. + */ +const isNumberInRange = (number: number, min: number, max: number) => { + return number >= min && number <= max; +}; + +export { isNumberInRange }; diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..4107ff0 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "moduleResolution": "node16", + "module": "Node16", + "lib": ["ES2015"], + "outDir": "./dist" + }, + "exclude": ["node_modules"], + "include": ["."] +} diff --git a/apps/api/tsup.config.ts b/apps/api/tsup.config.ts new file mode 100644 index 0000000..ba655f6 --- /dev/null +++ b/apps/api/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig, type Options } from 'tsup'; + +export default defineConfig((options: Options) => ({ + entryPoints: ['src/index.ts'], + clean: true, + format: ['cjs'], + ...options, +})); diff --git a/apps/api/turbo.json b/apps/api/turbo.json new file mode 100644 index 0000000..52e8c76 --- /dev/null +++ b/apps/api/turbo.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "//" + ], + "tasks": { + "build": { + "outputs": [ + "dist/**" + ] + } + } +} diff --git a/apps/frontend/.eslintrc.js b/apps/frontend/.eslintrc.js new file mode 100644 index 0000000..44a3f93 --- /dev/null +++ b/apps/frontend/.eslintrc.js @@ -0,0 +1,23 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ['@repo/eslint-config/react.js'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + }, + ignorePatterns: ['tailwind.config.js'], + rules: { + 'unicorn/filename-case': [ + 'error', + { + cases: { + kebabCase: true, + pascalCase: true, + camelCase: true, + }, + ignore: ['FeaturedGames.tsx'], + }, + ], + 'eslint-comments/require-description': 'off', + }, +}; diff --git a/apps/frontend/components.json b/apps/frontend/components.json new file mode 100644 index 0000000..cd5af28 --- /dev/null +++ b/apps/frontend/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/apps/frontend/index.html b/apps/frontend/index.html new file mode 100644 index 0000000..8aa45cc --- /dev/null +++ b/apps/frontend/index.html @@ -0,0 +1,21 @@ + + + + + + Admin | Kitchen Sink + + + + + + +
+ + + diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000..fe9dec3 --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,67 @@ +{ + "name": "frontend", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "vite build", + "clean": "rm -rf dist", + "dev": "vite --host 0.0.0.0 --port 3000 --clearScreen false", + "typecheck": "tsc --noEmit", + "lint": "eslint src/", + "add:ui": "pnpm dlx shadcn@latest add", + "preview": "vite preview" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@icons-pack/react-simple-icons": "^10.0.0", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", + "@repo/common": "workspace:*", + "@repo/db": "workspace:*", + "@tanstack/react-query": "^5.53.1", + "@tanstack/react-router": "^1.51.7", + "@tanstack/react-table": "^8.21.2", + "axios": "^1.7.6", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "crypto-browserify": "^3.12.0", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.5.2", + "lodash": "^4.17.21", + "lucide-react": "^0.436.0", + "motion": "^12.6.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-spinners": "^0.15.0", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@tanstack/router-devtools": "^1.51.7", + "@tanstack/router-plugin": "^1.51.6", + "@types/lodash": "^4.17.7", + "@types/react": "^18.2.62", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "typescript": "^5.3.3", + "vite": "^5.1.4", + "vite-plugin-node-polyfills": "^0.23.0", + "vite-tsconfig-paths": "^5.0.1" + } +} diff --git a/frontend/postcss.config.js b/apps/frontend/postcss.config.js similarity index 73% rename from frontend/postcss.config.js rename to apps/frontend/postcss.config.js index 2e7af2b..12a703d 100644 --- a/frontend/postcss.config.js +++ b/apps/frontend/postcss.config.js @@ -1,6 +1,6 @@ -export default { +module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/apps/frontend/public/favicon.ico b/apps/frontend/public/favicon.ico new file mode 100644 index 0000000..3a3c34a Binary files /dev/null and b/apps/frontend/public/favicon.ico differ diff --git a/apps/frontend/public/games/blackjack/background.svg b/apps/frontend/public/games/blackjack/background.svg new file mode 100644 index 0000000..ee15cd5 --- /dev/null +++ b/apps/frontend/public/games/blackjack/background.svg @@ -0,0 +1,112 @@ + + + + +Untitled-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/games/blackjack/double.svg b/apps/frontend/public/games/blackjack/double.svg new file mode 100644 index 0000000..6702871 --- /dev/null +++ b/apps/frontend/public/games/blackjack/double.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/public/games/blackjack/hit.svg b/apps/frontend/public/games/blackjack/hit.svg new file mode 100644 index 0000000..e3834e3 --- /dev/null +++ b/apps/frontend/public/games/blackjack/hit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/public/games/blackjack/split.svg b/apps/frontend/public/games/blackjack/split.svg new file mode 100644 index 0000000..53c1385 --- /dev/null +++ b/apps/frontend/public/games/blackjack/split.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/public/games/blackjack/stand.svg b/apps/frontend/public/games/blackjack/stand.svg new file mode 100644 index 0000000..1741dd4 --- /dev/null +++ b/apps/frontend/public/games/blackjack/stand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/public/games/cards/C.png b/apps/frontend/public/games/cards/C.png new file mode 100644 index 0000000..ef50e0c Binary files /dev/null and b/apps/frontend/public/games/cards/C.png differ diff --git a/apps/frontend/public/games/cards/D.png b/apps/frontend/public/games/cards/D.png new file mode 100644 index 0000000..aaa7899 Binary files /dev/null and b/apps/frontend/public/games/cards/D.png differ diff --git a/apps/frontend/public/games/cards/H.png b/apps/frontend/public/games/cards/H.png new file mode 100644 index 0000000..309fc41 Binary files /dev/null and b/apps/frontend/public/games/cards/H.png differ diff --git a/apps/frontend/public/games/cards/S.png b/apps/frontend/public/games/cards/S.png new file mode 100644 index 0000000..c58c0f3 Binary files /dev/null and b/apps/frontend/public/games/cards/S.png differ diff --git a/apps/frontend/public/games/cards/back.png b/apps/frontend/public/games/cards/back.png new file mode 100644 index 0000000..25efedc Binary files /dev/null and b/apps/frontend/public/games/cards/back.png differ diff --git a/apps/frontend/public/games/dice/loading-dice.png b/apps/frontend/public/games/dice/loading-dice.png new file mode 100644 index 0000000..c34cc95 Binary files /dev/null and b/apps/frontend/public/games/dice/loading-dice.png differ diff --git a/apps/frontend/public/games/dice/result-dice.png b/apps/frontend/public/games/dice/result-dice.png new file mode 100644 index 0000000..8e68a59 Binary files /dev/null and b/apps/frontend/public/games/dice/result-dice.png differ diff --git a/apps/frontend/public/games/keno/gem.svg b/apps/frontend/public/games/keno/gem.svg new file mode 100644 index 0000000..dc3ba2e --- /dev/null +++ b/apps/frontend/public/games/keno/gem.svg @@ -0,0 +1 @@ +gem \ No newline at end of file diff --git a/apps/frontend/public/games/mines/bomb-effect.webp b/apps/frontend/public/games/mines/bomb-effect.webp new file mode 100644 index 0000000..cae8da8 Binary files /dev/null and b/apps/frontend/public/games/mines/bomb-effect.webp differ diff --git a/apps/frontend/public/games/mines/bomb.png b/apps/frontend/public/games/mines/bomb.png new file mode 100644 index 0000000..9c7e777 Binary files /dev/null and b/apps/frontend/public/games/mines/bomb.png differ diff --git a/apps/frontend/public/games/mines/diamond.svg b/apps/frontend/public/games/mines/diamond.svg new file mode 100644 index 0000000..e9ce8c8 --- /dev/null +++ b/apps/frontend/public/games/mines/diamond.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/public/games/mines/mine.svg b/apps/frontend/public/games/mines/mine.svg new file mode 100644 index 0000000..37ed06d --- /dev/null +++ b/apps/frontend/public/games/mines/mine.svg @@ -0,0 +1 @@ +bomb diff --git a/apps/frontend/public/games/roulette/chip-bg-img.svg b/apps/frontend/public/games/roulette/chip-bg-img.svg new file mode 100644 index 0000000..d64955c --- /dev/null +++ b/apps/frontend/public/games/roulette/chip-bg-img.svg @@ -0,0 +1 @@ +background \ No newline at end of file diff --git a/apps/frontend/public/games/roulette/loading-roulette.png b/apps/frontend/public/games/roulette/loading-roulette.png new file mode 100644 index 0000000..8eadcc3 Binary files /dev/null and b/apps/frontend/public/games/roulette/loading-roulette.png differ diff --git a/apps/frontend/public/games/roulette/roulette-dolly.svg b/apps/frontend/public/games/roulette/roulette-dolly.svg new file mode 100644 index 0000000..d8fbf36 --- /dev/null +++ b/apps/frontend/public/games/roulette/roulette-dolly.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/apps/frontend/public/games/roulette/roulette-wheel-arrow.svg b/apps/frontend/public/games/roulette/roulette-wheel-arrow.svg new file mode 100644 index 0000000..300e8d3 --- /dev/null +++ b/apps/frontend/public/games/roulette/roulette-wheel-arrow.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/apps/frontend/src/_public/login.tsx b/apps/frontend/src/_public/login.tsx new file mode 100644 index 0000000..2798544 --- /dev/null +++ b/apps/frontend/src/_public/login.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import Login from '@/features/auth'; + +export const Route = createFileRoute('/_public/login')({ + component: Login, +}); diff --git a/apps/frontend/src/api/_utils/axiosInstance.ts b/apps/frontend/src/api/_utils/axiosInstance.ts new file mode 100644 index 0000000..fc25dee --- /dev/null +++ b/apps/frontend/src/api/_utils/axiosInstance.ts @@ -0,0 +1,27 @@ +import type { AxiosError } from 'axios'; +import axios from 'axios'; + +export const setupInterceptors = ({ + authErrCb, +}: { + authErrCb: () => void; +}): void => { + axios.interceptors.request.use(config => { + return { + ...config, + withCredentials: true, + }; + }); + + axios.interceptors.response.use( + response => { + return Promise.resolve(response); + }, + (error: AxiosError) => { + if (error.response?.status === 401) { + authErrCb(); + } + return Promise.reject(error); + } + ); +}; diff --git a/apps/frontend/src/api/_utils/fetch.ts b/apps/frontend/src/api/_utils/fetch.ts new file mode 100644 index 0000000..2befbcc --- /dev/null +++ b/apps/frontend/src/api/_utils/fetch.ts @@ -0,0 +1,19 @@ +import axios, { type AxiosRequestConfig } from 'axios'; +import { BASE_API_URL } from '@/const/routes'; + +export const fetchGet = async ( + url: string, + config?: AxiosRequestConfig +): Promise => { + const response = await axios.get(BASE_API_URL + url, config); + return response.data; +}; + +export const fetchPost = async ( + url: string, + data: T, + config?: AxiosRequestConfig +): Promise => { + const response = await axios.post(BASE_API_URL + url, data, config); + return response.data; +}; diff --git a/apps/frontend/src/api/auth.ts b/apps/frontend/src/api/auth.ts new file mode 100644 index 0000000..3c1d77b --- /dev/null +++ b/apps/frontend/src/api/auth.ts @@ -0,0 +1,8 @@ +import type { IUser, ApiResponse } from '@repo/common/types'; +import { fetchGet } from './_utils/fetch'; + +export const getUserDetails = async (): Promise> => { + return fetchGet('/api/v1/auth/me', { + withCredentials: true, + }); +}; diff --git a/apps/frontend/src/api/balance.ts b/apps/frontend/src/api/balance.ts new file mode 100644 index 0000000..0739e31 --- /dev/null +++ b/apps/frontend/src/api/balance.ts @@ -0,0 +1,16 @@ +import type { ApiResponse } from '@repo/common/types'; +import { fetchGet } from './_utils/fetch'; + +interface BalanceResponse { + balance: number; +} + +export const getBalance = async (): Promise => { + const { data } = await fetchGet>>( + '/api/v1/user/balance', + { + withCredentials: true, + } + ); + return data.balance; +}; diff --git a/apps/frontend/src/api/games/blackjack.ts b/apps/frontend/src/api/games/blackjack.ts new file mode 100644 index 0000000..cc4db61 --- /dev/null +++ b/apps/frontend/src/api/games/blackjack.ts @@ -0,0 +1,40 @@ +import type { + BlackjackActions, + BlackjackPlayRoundResponse, +} from '@repo/common/game-utils/blackjack/types.js'; +import type { ApiResponse } from '@repo/common/types'; +import { fetchGet, fetchPost } from '../_utils/fetch'; + +export const blackjackBet = async ({ + betAmount, +}: { + betAmount: number; +}): Promise> => { + return fetchPost( + '/api/v1/games/blackjack/bet', + { betAmount }, + { + withCredentials: true, + } + ); +}; + +export const getActiveGame = async (): Promise< + ApiResponse +> => { + return fetchGet('/api/v1/games/blackjack/active', { + withCredentials: true, + }); +}; + +export const playRound = async ( + action: BlackjackActions +): Promise> => { + return fetchPost( + '/api/v1/games/blackjack/next', + { action }, + { + withCredentials: true, + } + ); +}; diff --git a/apps/frontend/src/api/games/dice.ts b/apps/frontend/src/api/games/dice.ts new file mode 100644 index 0000000..fc90cee --- /dev/null +++ b/apps/frontend/src/api/games/dice.ts @@ -0,0 +1,18 @@ +import type { ApiResponse } from '@repo/common/types'; +import type { + DicePlaceBetRequestBody, + DicePlaceBetResponse, +} from '@repo/common/game-utils/dice/types.js'; +import { fetchPost } from '../_utils/fetch'; + +export const placeBet = async ( + data: DicePlaceBetRequestBody +): Promise> => { + return fetchPost>( + '/api/v1/games/dice/place-bet', + data, + { + withCredentials: true, + } + ); +}; diff --git a/apps/frontend/src/api/games/keno.ts b/apps/frontend/src/api/games/keno.ts new file mode 100644 index 0000000..e901938 --- /dev/null +++ b/apps/frontend/src/api/games/keno.ts @@ -0,0 +1,21 @@ +import type { ApiResponse } from '@repo/common/types'; +import type { KenoResponse } from '@repo/common/game-utils/keno/types.js'; +import { fetchPost } from '../_utils/fetch'; + +export const placeBet = async ({ + betAmount, + selectedTiles, + risk, +}: { + betAmount: number; + selectedTiles: number[]; + risk: string; +}): Promise> => { + return fetchPost( + '/api/v1/games/keno/place-bet', + { betAmount, selectedTiles, risk }, + { + withCredentials: true, + } + ); +}; diff --git a/apps/frontend/src/api/games/mines.ts b/apps/frontend/src/api/games/mines.ts new file mode 100644 index 0000000..a075d2e --- /dev/null +++ b/apps/frontend/src/api/games/mines.ts @@ -0,0 +1,50 @@ +import type { ApiResponse } from '@repo/common/types'; +import type { + MinesGameOverResponse, + MinesPlayRoundResponse, +} from '@repo/common/game-utils/mines/types.js'; +import { fetchGet, fetchPost } from '../_utils/fetch'; + +export const startGame = async ({ + betAmount, + minesCount, +}: { + betAmount: number; + minesCount: number; +}): Promise> => { + return fetchPost( + '/api/v1/games/mines/start', + { betAmount, minesCount }, + { + withCredentials: true, + } + ); +}; + +export const playRound = async ( + selectedTileIndex: number +): Promise> => { + return fetchPost( + '/api/v1/games/mines/play-round', + { selectedTileIndex }, + { + withCredentials: true, + } + ); +}; + +export const cashOut = async (): Promise< + ApiResponse +> => { + return fetchPost('/api/v1/games/mines/cash-out', { + withCredentials: true, + }); +}; + +export const getActiveGame = async (): Promise< + ApiResponse +> => { + return fetchGet('/api/v1/games/mines/active', { + withCredentials: true, + }); +}; diff --git a/apps/frontend/src/api/games/roulette.ts b/apps/frontend/src/api/games/roulette.ts new file mode 100644 index 0000000..f0ec50d --- /dev/null +++ b/apps/frontend/src/api/games/roulette.ts @@ -0,0 +1,18 @@ +import type { ApiResponse } from '@repo/common/types'; +import type { + RouletteBet, + RoulettePlaceBetResponse, +} from '@repo/common/game-utils/roulette/index.js'; +import { fetchPost } from '../_utils/fetch'; + +export const placeBet = async ( + bets: RouletteBet[] +): Promise> => { + return fetchPost( + '/api/v1/games/roulette/place-bet', + { bets }, + { + withCredentials: true, + } + ); +}; diff --git a/apps/frontend/src/api/user.ts b/apps/frontend/src/api/user.ts new file mode 100644 index 0000000..8a02e9a --- /dev/null +++ b/apps/frontend/src/api/user.ts @@ -0,0 +1,46 @@ +import type { + ProvablyFairStateResponse, + ApiResponse, + PaginatedBetsResponse, +} from '@repo/common/types'; +import { fetchGet, fetchPost } from './_utils/fetch'; + +export const fetchActiveSeeds = async (): Promise< + ApiResponse +> => + fetchGet('/api/v1/user/provably-fair-state', { + withCredentials: true, + }); + +export const fetchRotateSeedPair = async ( + clientSeed: string +): Promise> => + fetchPost( + '/api/v1/user/rotate-seeds', + { clientSeed }, + { + withCredentials: true, + } + ); + +export const fetchRevealedServerSeed = async ( + hashedServerSeed: string +): Promise> => + fetchGet(`/api/v1/user/unhash-server-seed/${hashedServerSeed}`, { + withCredentials: true, + }); + +export const fetchUserBetHistory = async ({ + page = 1, + pageSize = 10, +}: { + page?: number; + pageSize?: number; +}): Promise> => + fetchGet('/api/v1/user/bets', { + withCredentials: true, + params: { + page, + pageSize, + }, + }); diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx new file mode 100644 index 0000000..73bc44c --- /dev/null +++ b/apps/frontend/src/app.tsx @@ -0,0 +1,34 @@ +import './index.css'; +import { createRouter, RouterProvider } from '@tanstack/react-router'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { routeTree } from './routeTree.gen'; +import type { AuthState } from './features/auth/store/authStore'; +import { useAuthStore } from './features/auth/store/authStore'; + +const queryClient = new QueryClient(); + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + context: { + authStore: undefined, + } as { authStore: AuthState | undefined }, +}); + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } +} + +function App(): JSX.Element { + const authStore = useAuthStore(); + + return ( + + + + ); +} + +export default App; diff --git a/apps/frontend/src/assets/audio/bet.mp3 b/apps/frontend/src/assets/audio/bet.mp3 new file mode 100644 index 0000000..b3b7ca3 Binary files /dev/null and b/apps/frontend/src/assets/audio/bet.mp3 differ diff --git a/apps/frontend/src/assets/audio/rolling.mp3 b/apps/frontend/src/assets/audio/rolling.mp3 new file mode 100644 index 0000000..74035f0 Binary files /dev/null and b/apps/frontend/src/assets/audio/rolling.mp3 differ diff --git a/apps/frontend/src/assets/audio/tick.mp3 b/apps/frontend/src/assets/audio/tick.mp3 new file mode 100644 index 0000000..c04e37a Binary files /dev/null and b/apps/frontend/src/assets/audio/tick.mp3 differ diff --git a/apps/frontend/src/assets/audio/win.mp3 b/apps/frontend/src/assets/audio/win.mp3 new file mode 100644 index 0000000..54313ae Binary files /dev/null and b/apps/frontend/src/assets/audio/win.mp3 differ diff --git a/apps/frontend/src/assets/game-icons/roulette/chip-bg-img.svg b/apps/frontend/src/assets/game-icons/roulette/chip-bg-img.svg new file mode 100644 index 0000000..e69de29 diff --git a/apps/frontend/src/assets/game-icons/roulette/roulette-icon.svg b/apps/frontend/src/assets/game-icons/roulette/roulette-icon.svg new file mode 100644 index 0000000..21bc523 --- /dev/null +++ b/apps/frontend/src/assets/game-icons/roulette/roulette-icon.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/frontend/src/common/forms/components/InputWithIcon.tsx b/apps/frontend/src/common/forms/components/InputWithIcon.tsx new file mode 100644 index 0000000..a01ecff --- /dev/null +++ b/apps/frontend/src/common/forms/components/InputWithIcon.tsx @@ -0,0 +1,39 @@ +import type { ReactNode } from 'react'; +import type { InputProps } from '@/components/ui/input'; +import { Input } from '@/components/ui/input'; +import { cn } from '@/lib/utils'; + +interface InputWithIconProps extends InputProps { + icon: ReactNode; + wrapperClassName?: string; +} + +function InputWithIcon({ + icon, + wrapperClassName, + ...inputProps +}: InputWithIconProps): JSX.Element { + return ( +
+ + {icon} +
+ ); +} + +export default InputWithIcon; diff --git a/apps/frontend/src/common/hooks/useAudio.ts b/apps/frontend/src/common/hooks/useAudio.ts new file mode 100644 index 0000000..20d2a57 --- /dev/null +++ b/apps/frontend/src/common/hooks/useAudio.ts @@ -0,0 +1,108 @@ +import { useCallback, useRef, useState } from 'react'; +import throttle from 'lodash/throttle'; + +const useAudio = ( + audioSrc: string, + volume = 1 +): { + isPlaying: boolean; + play: () => Promise; + playThrottled: () => void; + playInfinite: () => void; + pause: () => void; + stop: () => void; + setVolume: (volume: number) => void; + setCurrentTime: (time: number) => void; +} => { + const [isPlaying, setIsPlaying] = useState(false); + const audioRef = useRef(new Audio(audioSrc)); + + const playThrottledRef = useRef( + throttle(() => { + const audio = new Audio(audioSrc); + audio.volume = volume; + audio + .play() + .then(() => { + setIsPlaying(true); + audio.onended = () => { + setIsPlaying(false); + }; + }) + .catch((error: Error) => { + return error; + }); + }, 2) + ); + + const play = async (): Promise => { + return new Promise((resolve, reject) => { + const audio = new Audio(audioSrc); + audio.volume = volume; + + audio + .play() + .then(() => { + setIsPlaying(true); + audio.onended = () => { + setIsPlaying(false); + resolve(); + }; + }) + .catch((error: Error) => { + reject(error); + }); + }); + }; + + const playInfinite = useCallback((): void => { + const audio = audioRef.current; + audio.loop = true; + audio.volume = volume; + audio + .play() + .then(() => { + setIsPlaying(true); + }) + .catch((error: Error) => { + return error; + }); + }, [volume]); + + const playThrottled = (): void => { + playThrottledRef.current(); + }; + + const pause = (): void => { + audioRef.current.pause(); + setIsPlaying(false); + }; + + const stop = useCallback((): void => { + audioRef.current.pause(); + audioRef.current.currentTime = 0; + audioRef.current.loop = false; + setIsPlaying(false); + }, []); + + const setVolume = (newVolume: number): void => { + audioRef.current.volume = newVolume; + }; + + const setCurrentTime = (time: number): void => { + audioRef.current.currentTime = time; + }; + + return { + isPlaying, + play, + playThrottled, + playInfinite, + pause, + stop, + setVolume, + setCurrentTime, + }; +}; + +export { useAudio }; diff --git a/apps/frontend/src/common/icons/dice.jsx b/apps/frontend/src/common/icons/dice.jsx new file mode 100644 index 0000000..0755129 --- /dev/null +++ b/apps/frontend/src/common/icons/dice.jsx @@ -0,0 +1,32 @@ +function Dice() { + return ( + + + + + 2Artboard 440 + + + + + + + ); +} + +export default Dice; diff --git a/apps/frontend/src/components/Header/Balance.tsx b/apps/frontend/src/components/Header/Balance.tsx new file mode 100644 index 0000000..9b3e541 --- /dev/null +++ b/apps/frontend/src/components/Header/Balance.tsx @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; +import { BadgeDollarSign } from 'lucide-react'; +import { getBalance } from '@/api/balance'; +import InputWithIcon from '@/common/forms/components/InputWithIcon'; + +export function Balance(): JSX.Element { + const { data: balance } = useQuery({ + queryKey: ['balance'], + queryFn: getBalance, + refetchInterval: 120000, + // Refetch every 2 minutes + }); + + return ( + } + value={balance?.toFixed(2)} + wrapperClassName="shadow-md w-60" + /> + ); +} diff --git a/apps/frontend/src/components/Header/index.tsx b/apps/frontend/src/components/Header/index.tsx new file mode 100644 index 0000000..932c478 --- /dev/null +++ b/apps/frontend/src/components/Header/index.tsx @@ -0,0 +1,11 @@ +import { Balance } from './Balance'; + +export function Header(): JSX.Element { + return ( +
+
+ +
+
+ ); +} diff --git a/apps/frontend/src/components/ui/avatar.tsx b/apps/frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000..4551323 --- /dev/null +++ b/apps/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; +import { cn } from '@/lib/utils'; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/frontend/src/components/ui/button.tsx b/apps/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..a18f44b --- /dev/null +++ b/apps/frontend/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: + 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + } +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/apps/frontend/src/components/ui/card.tsx b/apps/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..22549bf --- /dev/null +++ b/apps/frontend/src/components/ui/card.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = 'Card'; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => ( +

+ {children} +

+)); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = 'CardDescription'; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = 'CardContent'; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = 'CardFooter'; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/apps/frontend/src/components/ui/carousel.tsx b/apps/frontend/src/components/ui/carousel.tsx new file mode 100644 index 0000000..a496b4b --- /dev/null +++ b/apps/frontend/src/components/ui/carousel.tsx @@ -0,0 +1,258 @@ +import * as React from 'react'; +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from 'embla-carousel-react'; +import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +interface CarouselProps { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: 'horizontal' | 'vertical'; + setApi?: (api: CarouselApi) => void; +} + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +export function useCarousel(): CarouselContextProps { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error('useCarousel must be used within a '); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = 'horizontal', + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y', + }, + plugins + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((carouselApi: CarouselApi) => { + if (!carouselApi) { + return; + } + + setCanScrollPrev(carouselApi.canScrollPrev()); + setCanScrollNext(carouselApi.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault(); + scrollPrev(); + } else if (event.key === 'ArrowRight') { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext] + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on('reInit', onSelect); + api.on('select', onSelect); + + return () => { + api.off('select', onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); + } +); +Carousel.displayName = 'Carousel'; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); +}); +CarouselContent.displayName = 'CarouselContent'; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); +}); +CarouselItem.displayName = 'CarouselItem'; + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); +}); +CarouselPrevious.displayName = 'CarouselPrevious'; + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +}); +CarouselNext.displayName = 'CarouselNext'; + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; diff --git a/apps/frontend/src/components/ui/common-data-table.tsx b/apps/frontend/src/components/ui/common-data-table.tsx new file mode 100644 index 0000000..bf8df7d --- /dev/null +++ b/apps/frontend/src/components/ui/common-data-table.tsx @@ -0,0 +1,161 @@ +import type { + ColumnDef, + PaginationState, + Updater, +} from '@tanstack/react-table'; +import { + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { + Table as TableUI, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from './table'; +import { Button } from './button'; + +interface CommonDataTableProps { + columns: ColumnDef[]; + data: TData[]; + pageCount: number; + setPagination: (updater: Updater) => void; + pagination: PaginationState; + rowCount: number; +} + +// Type declaration to extend ColumnDef with our custom meta properties +declare module '@tanstack/react-table' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface ColumnMeta { + alignment?: 'left' | 'right' | 'center'; + } +} + +export function CommonDataTable({ + columns, + data, + pageCount, + setPagination, + pagination, + rowCount, +}: CommonDataTableProps): JSX.Element { + const table = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount, + rowCount, + defaultColumn: { + size: 200, //starting column size + }, + columnResizeMode: 'onChange', + onPaginationChange: updater => { + // Handle the updater whether it's a function or an object + if (typeof updater === 'function') { + const newState = updater(pagination); + setPagination({ + pageIndex: newState.pageIndex, + pageSize: newState.pageSize, + }); + } else { + setPagination({ + pageIndex: updater.pageIndex, + pageSize: updater.pageSize, + }); + } + }, + state: { + pagination, + }, + }); + + return ( +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + + + {/* Pagination controls */} +
+ + +
+
+ ); +} diff --git a/apps/frontend/src/components/ui/common-select.tsx b/apps/frontend/src/components/ui/common-select.tsx new file mode 100644 index 0000000..7522663 --- /dev/null +++ b/apps/frontend/src/components/ui/common-select.tsx @@ -0,0 +1,64 @@ +import type { ReactNode } from 'react'; +import { cn } from '@/lib/utils'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './select'; +import { Label } from './label'; + +interface CommonSelectProps { + label: ReactNode; + options: { + label: ReactNode; + value: string; + }[]; + onValueChange: (value: string) => void; + value: string | null; + triggerClassName?: string; + labelClassName?: string; +} + +function CommonSelect({ + label, + options, + onValueChange, + value, + triggerClassName, + labelClassName, +}: CommonSelectProps): JSX.Element { + return ( +
+ + +
+ ); +} + +export default CommonSelect; diff --git a/apps/frontend/src/components/ui/common-tooltip/index.tsx b/apps/frontend/src/components/ui/common-tooltip/index.tsx new file mode 100644 index 0000000..d0da475 --- /dev/null +++ b/apps/frontend/src/components/ui/common-tooltip/index.tsx @@ -0,0 +1,44 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; + +interface CommonTooltipProps { + children: React.ReactNode; + content: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; + forceHide?: boolean; +} + +function CommonTooltip({ + children, + content, + open, + onOpenChange = () => { + void 0; + }, + forceHide = false, +}: CommonTooltipProps): JSX.Element { + if (forceHide) { + return <>{children}; + } + return ( + + + {children} + + {content} + + + + ); +} + +export default CommonTooltip; diff --git a/apps/frontend/src/components/ui/dialog.tsx b/apps/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..baba93d --- /dev/null +++ b/apps/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,123 @@ +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { Cross2Icon } from '@radix-ui/react-icons'; +import { cn } from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +function DialogHeader({ + className, + ...props +}: React.HTMLAttributes): JSX.Element { + return ( +
+ ); +} +DialogHeader.displayName = 'DialogHeader'; + +function DialogFooter({ + className, + ...props +}: React.HTMLAttributes): JSX.Element { + return ( +
+ ); +} +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/apps/frontend/src/components/ui/dice-result-slider.tsx b/apps/frontend/src/components/ui/dice-result-slider.tsx new file mode 100644 index 0000000..41ae0ba --- /dev/null +++ b/apps/frontend/src/components/ui/dice-result-slider.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; +import { cn } from '@/lib/utils'; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + success?: boolean; + } +>(({ className, success, ...props }, ref) => { + return ( + props.value && ( + + +
+ Result Dice +
+ {props.value[0]} +
+
+
+
+ ) + ); +}); + +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/apps/frontend/src/components/ui/dice-slider.tsx b/apps/frontend/src/components/ui/dice-slider.tsx new file mode 100644 index 0000000..de086e9 --- /dev/null +++ b/apps/frontend/src/components/ui/dice-slider.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; +import { Tally3 } from 'lucide-react'; +import type { DiceCondition } from '@repo/common/game-utils/dice/types.js'; +import { cn } from '@/lib/utils'; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + condition: DiceCondition; + } +>(({ className, condition, ...props }, ref) => { + return ( + + + + + +
+ +
+
+
+ ); +}); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/apps/frontend/src/components/ui/dropdown-menu.tsx b/apps/frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..77bdb11 --- /dev/null +++ b/apps/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,202 @@ +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from '@radix-ui/react-icons'; +import { cn } from '@/lib/utils'; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +function DropdownMenuShortcut({ + className, + ...props +}: React.HTMLAttributes): JSX.Element { + return ( + + ); +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/apps/frontend/src/components/ui/input.tsx b/apps/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..ab9d582 --- /dev/null +++ b/apps/frontend/src/components/ui/input.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +export type InputProps = React.InputHTMLAttributes; + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = 'Input'; + +export { Input }; diff --git a/apps/frontend/src/components/ui/label.tsx b/apps/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..b6472ff --- /dev/null +++ b/apps/frontend/src/components/ui/label.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const labelVariants = cva( + 'text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-[#b1bad3]' +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/apps/frontend/src/components/ui/playing-card.tsx b/apps/frontend/src/components/ui/playing-card.tsx new file mode 100644 index 0000000..2d5fd81 --- /dev/null +++ b/apps/frontend/src/components/ui/playing-card.tsx @@ -0,0 +1,82 @@ +import { CardSuits } from '@repo/common/game-utils/cards/types.js'; +import { motion } from 'motion/react'; +import { cn } from '@/lib/utils'; + +function PlayingCard({ + faceDown = false, + layoutId, + rank, + suit, +}: { + rank?: string; + suit?: CardSuits; + faceDown?: boolean; + layoutId?: string; +}): JSX.Element { + return ( + + {/* Card Front Face */} +
+ {rank && suit ? ( +
+ + {rank} + + {`${suit} +
+ ) : ( +
+ )} +
+ + {/* Card Back Face */} +
+ Card back +
+ + ); +} + +export default PlayingCard; diff --git a/apps/frontend/src/components/ui/select.tsx b/apps/frontend/src/components/ui/select.tsx new file mode 100644 index 0000000..bce20a5 --- /dev/null +++ b/apps/frontend/src/components/ui/select.tsx @@ -0,0 +1,160 @@ +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from '@radix-ui/react-icons'; +import { cn } from '@/lib/utils'; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + className + )} + ref={ref} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + {/* */} + + {children} + + {/* */} + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/apps/frontend/src/components/ui/separator.tsx b/apps/frontend/src/components/ui/separator.tsx new file mode 100644 index 0000000..b56a73c --- /dev/null +++ b/apps/frontend/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import * as SeparatorPrimitive from '@radix-ui/react-separator'; +import { cn } from '@/lib/utils'; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = 'horizontal', decorative = true, ...props }, + ref + ) => ( + + ) +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/apps/frontend/src/components/ui/slider.tsx b/apps/frontend/src/components/ui/slider.tsx new file mode 100644 index 0000000..5c954b5 --- /dev/null +++ b/apps/frontend/src/components/ui/slider.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; +import { cn } from '@/lib/utils'; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/apps/frontend/src/components/ui/table.tsx b/apps/frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..dddfdcb --- /dev/null +++ b/apps/frontend/src/components/ui/table.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); +Table.displayName = 'Table'; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = 'TableHeader'; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = 'TableBody'; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', + className + )} + ref={ref} + {...props} + /> +)); +TableFooter.displayName = 'TableFooter'; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = 'TableRow'; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]', + className + )} + ref={ref} + {...props} + /> +)); +TableHead.displayName = 'TableHead'; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]', + className + )} + ref={ref} + {...props} + /> +)); +TableCell.displayName = 'TableCell'; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = 'TableCaption'; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/apps/frontend/src/components/ui/tabs.tsx b/apps/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000..d6e2784 --- /dev/null +++ b/apps/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import * as TabsPrimitive from '@radix-ui/react-tabs'; +import { cn } from '@/lib/utils'; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/apps/frontend/src/components/ui/tooltip.tsx b/apps/frontend/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..4f322a3 --- /dev/null +++ b/apps/frontend/src/components/ui/tooltip.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; +import { cn } from '@/lib/utils'; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + {props.children} + + + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/apps/frontend/src/const/games.ts b/apps/frontend/src/const/games.ts new file mode 100644 index 0000000..1da34f4 --- /dev/null +++ b/apps/frontend/src/const/games.ts @@ -0,0 +1,49 @@ +import { DicesIcon, ShipWheelIcon } from 'lucide-react'; + +export enum Games { + DICE = 'dice', + ROULETTE = 'roulette', + MINES = 'mines', + KENO = 'keno', + BLACKJACK = 'blackjack', +} + +export type Game = (typeof Games)[keyof typeof Games]; + +export const GAME_VALUES_MAPPING = { + [Games.DICE]: { label: 'Dice', icon: DicesIcon, path: '/casino/games/dice' }, + [Games.ROULETTE]: { + label: 'Roulette', + icon: ShipWheelIcon, + path: '/casino/games/roulette', + }, + [Games.MINES]: { + label: 'Mines', + // icon: DicesIcon, + path: '/casino/games/mines', + }, + [Games.KENO]: { + label: 'Keno', + // icon: DicesIcon, + path: '/casino/games/keno', + }, +}; + +export const GAMES_DROPDOWN_OPTIONS = [ + { + label: GAME_VALUES_MAPPING[Games.DICE].label, + value: Games.DICE, + }, + { + label: GAME_VALUES_MAPPING[Games.ROULETTE].label, + value: Games.ROULETTE, + }, + { + label: GAME_VALUES_MAPPING[Games.MINES].label, + value: Games.MINES, + }, + { + label: GAME_VALUES_MAPPING[Games.KENO].label, + value: Games.KENO, + }, +]; diff --git a/apps/frontend/src/const/routes.ts b/apps/frontend/src/const/routes.ts new file mode 100644 index 0000000..79127dd --- /dev/null +++ b/apps/frontend/src/const/routes.ts @@ -0,0 +1,3 @@ +export const BASE_API_URL = + (import.meta.env.VITE_APP_API_URL as string | undefined) || + 'http://localhost:5000'; diff --git a/apps/frontend/src/features/auth/components/LoginModal.tsx b/apps/frontend/src/features/auth/components/LoginModal.tsx new file mode 100644 index 0000000..1f1218a --- /dev/null +++ b/apps/frontend/src/features/auth/components/LoginModal.tsx @@ -0,0 +1,70 @@ +import { useEffect } from 'react'; +import { SiGoogle } from '@icons-pack/react-simple-icons'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { useAuthStore } from '../store/authStore'; + +export function LoginModal(): JSX.Element { + const { user, isModalOpen, hideLoginModal } = useAuthStore(); + + const handleGoogleLogin = (): void => { + // Save current URL to redirect back after login + const currentUrl = window.location.href; + localStorage.setItem('auth_redirect', currentUrl); + + // Redirect to Google OAuth endpoint + window.location.href = 'http://localhost:5000/api/v1/auth/google'; + }; + + // Close the modal when user becomes authenticated + useEffect(() => { + if (user) { + hideLoginModal(); + } + }, [user, hideLoginModal]); + + return ( + + + + + Welcome to Fake Stake + + + +
+ + +
+
+ +
+
+ +

+ By continuing, you agree to our{' '} + {' '} + and{' '} + +

+
+
+
+ ); +} diff --git a/apps/frontend/src/features/auth/const/immutableConst.ts b/apps/frontend/src/features/auth/const/immutableConst.ts new file mode 100644 index 0000000..0b7e82e --- /dev/null +++ b/apps/frontend/src/features/auth/const/immutableConst.ts @@ -0,0 +1 @@ +export const key = 'user'; diff --git a/apps/frontend/src/features/auth/index.tsx b/apps/frontend/src/features/auth/index.tsx new file mode 100644 index 0000000..3e5a188 --- /dev/null +++ b/apps/frontend/src/features/auth/index.tsx @@ -0,0 +1,70 @@ +import { SiGoogle } from '@icons-pack/react-simple-icons'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; + +export default function Login(): JSX.Element { + const handleGoogleLogin = (): void => { + window.location.href = 'http://localhost:5000/api/v1/auth/google'; + }; + + return ( +
+
+

Login

+

+ Welcome back! Sign in to your account. +

+
+
+ +
+
+ +
+
+ + Or continue with + +
+
+ {/*
+ + +
*/} +
+ + +
+
+ + +
+ +
+ + +
+
+
+ ); +} diff --git a/apps/frontend/src/features/auth/store/authStore.ts b/apps/frontend/src/features/auth/store/authStore.ts new file mode 100644 index 0000000..8f6fb17 --- /dev/null +++ b/apps/frontend/src/features/auth/store/authStore.ts @@ -0,0 +1,37 @@ +import type { IUser } from '@repo/common/types'; +import { create } from 'zustand'; +import { getStoredUser, setStoredUser } from '../utils/storage'; + +export interface AuthState { + user: IUser | null; + isModalOpen: boolean; + setUser: (user: IUser | null) => void; + showLoginModal: () => void; + hideLoginModal: () => void; +} + +export const useAuthStore = create(set => ({ + user: getStoredUser(), + isModalOpen: false, + setUser: user => { + setStoredUser(user); + set({ user }); + }, + showLoginModal: () => { + set({ isModalOpen: true }); + }, + hideLoginModal: () => { + set({ isModalOpen: false }); + }, +})); + +export const getAuthState = (): AuthState => { + const state = useAuthStore.getState(); + return { + user: state.user, + isModalOpen: state.isModalOpen, + setUser: state.setUser, + showLoginModal: state.showLoginModal, + hideLoginModal: state.hideLoginModal, + }; +}; diff --git a/apps/frontend/src/features/auth/utils/storage.ts b/apps/frontend/src/features/auth/utils/storage.ts new file mode 100644 index 0000000..1ea7333 --- /dev/null +++ b/apps/frontend/src/features/auth/utils/storage.ts @@ -0,0 +1,15 @@ +import type { IUser } from '@repo/common/types'; +import { key } from '../const/immutableConst'; + +export const getStoredUser = (): IUser | null => { + const user = localStorage.getItem(key); + return user ? (JSON.parse(user) as IUser) : null; +}; + +export const setStoredUser = (user: IUser | null): void => { + if (user) { + localStorage.setItem(key, JSON.stringify(user)); + } else { + localStorage.removeItem(key); + } +}; diff --git a/apps/frontend/src/features/games/blackjack/components/BettingControls.tsx b/apps/frontend/src/features/games/blackjack/components/BettingControls.tsx new file mode 100644 index 0000000..cd24e7c --- /dev/null +++ b/apps/frontend/src/features/games/blackjack/components/BettingControls.tsx @@ -0,0 +1,144 @@ +import React, { useEffect } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { getValidActionsFromState } from '@repo/common/game-utils/blackjack/utils.js'; +import { BlackjackActions } from '@repo/common/game-utils/blackjack/types.js'; +import { blackjackBet, getActiveGame, playRound } from '@/api/games/blackjack'; +import { Button } from '@/components/ui/button'; +import useBlackjackStore from '../store/blackjackStore'; +import { BetAmountInput } from '../../common/components/BetAmountInput'; +import { BetButton } from '../../common/components/BettingControls'; + +const BlackjackActionButtons = [ + { + label: 'Hit', + value: BlackjackActions.HIT, + icon: '/games/blackjack/hit.svg', + }, + { + label: 'Stand', + value: BlackjackActions.STAND, + icon: '/games/blackjack/stand.svg', + }, + { + label: 'Split', + value: BlackjackActions.SPLIT, + icon: '/games/blackjack/split.svg', + }, + { + label: 'Double', + value: BlackjackActions.DOUBLE, + icon: '/games/blackjack/double.svg', + }, +]; + +function BettingControls(): JSX.Element { + const { betAmount, setBetAmount, gameState, setGameState } = + useBlackjackStore(); + + const { + isPending: isFetchingActiveGame, + data: activeGame, + isError, + } = useQuery({ + queryKey: ['blackjack-active-game'], + queryFn: getActiveGame, + retry: false, + }); + + const { mutate: bet, isPending: isStartingGame } = useMutation({ + mutationKey: ['blackjack-bet'], + mutationFn: () => blackjackBet({ betAmount }), + onSuccess: ({ data }) => { + setGameState(data, false); + setBetAmount(Number(data.betAmount)); + }, + }); + + const { mutate: playNextRound, isPending: isPlayingRound } = useMutation({ + mutationKey: ['blackjack-play-round'], + mutationFn: (action: BlackjackActions) => playRound(action), + onSuccess: ({ data }) => { + setGameState(data, false); + setBetAmount(Number(data.betAmount)); + }, + }); + + const queryClient = useQueryClient(); + const balance = queryClient.getQueryData(['balance']); + const isDisabled = betAmount > (balance ?? 0) || betAmount <= 0; + + const validActions = getValidActionsFromState({ + state: gameState?.state || null, + active: gameState?.active || false, + }); + + return ( +
+
+ { + setBetAmount(amount * multiplier); + }} + /> +
+ {validActions.insurance ? ( + <> +
+ Insurance? +
+ + + + ) : ( + BlackjackActionButtons.map(action => ( + + )) + )} + {} +
+
+ + +
+ ); +} + +export default BettingControls; diff --git a/apps/frontend/src/features/games/blackjack/const/index.ts b/apps/frontend/src/features/games/blackjack/const/index.ts new file mode 100644 index 0000000..5c576f1 --- /dev/null +++ b/apps/frontend/src/features/games/blackjack/const/index.ts @@ -0,0 +1,3 @@ +const FACE_DOWN_CARD_IDS = ['deck-1', 'deck-2', 'deck-3', 'deck-4', 'deck-5']; + +export { FACE_DOWN_CARD_IDS }; diff --git a/apps/frontend/src/features/games/blackjack/index.tsx b/apps/frontend/src/features/games/blackjack/index.tsx new file mode 100644 index 0000000..dd913db --- /dev/null +++ b/apps/frontend/src/features/games/blackjack/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Games } from '@/const/games'; +import GameSettingsBar from '../common/components/game-settings'; +import BettingControls from './components/BettingControls'; + +export default function Blackjack(): JSX.Element { + return ( + <> +
+ +
+
+ + + ); +} diff --git a/apps/frontend/src/features/games/blackjack/store/blackjackStore.ts b/apps/frontend/src/features/games/blackjack/store/blackjackStore.ts new file mode 100644 index 0000000..240928d --- /dev/null +++ b/apps/frontend/src/features/games/blackjack/store/blackjackStore.ts @@ -0,0 +1,41 @@ +import type { + // MinesGameOverResponse, + BlackjackPlayRoundResponse, +} from '@repo/common/game-utils/blackjack/types.js'; +import { create } from 'zustand'; + +interface BlackjackStore { + betAmount: number; + setBetAmount: (betAmount: number) => void; + + gameState: BlackjackPlayRoundResponse | null; + prevGameState: BlackjackPlayRoundResponse | null; + setGameState: ( + gameState: BlackjackPlayRoundResponse | null, + flipped: boolean + ) => void; + + flippedCards: Record>; + incomingCards: Record>; + + clearTransientCards: () => void; +} + +const useBlackjackStore = create((set, get) => ({ + betAmount: 0, + setBetAmount: betAmount => { + set({ betAmount }); + }, + gameState: null, + prevGameState: null, + + flippedCards: {}, + incomingCards: {}, + setGameState: () => {}, + + clearTransientCards: () => { + set({ incomingCards: {} }); + }, +})); + +export default useBlackjackStore; diff --git a/apps/frontend/src/features/games/common/components/BetAmountButton.tsx b/apps/frontend/src/features/games/common/components/BetAmountButton.tsx new file mode 100644 index 0000000..e9e41c3 --- /dev/null +++ b/apps/frontend/src/features/games/common/components/BetAmountButton.tsx @@ -0,0 +1,21 @@ +import { Button } from '@/components/ui/button'; + +export function BetAmountButton({ + label, + onClick, + disabled, +}: { + label: string; + onClick: () => void; + disabled?: boolean; +}): JSX.Element { + return ( + + ); +} diff --git a/apps/frontend/src/features/games/common/components/BetAmountInput.tsx b/apps/frontend/src/features/games/common/components/BetAmountInput.tsx new file mode 100644 index 0000000..25b4396 --- /dev/null +++ b/apps/frontend/src/features/games/common/components/BetAmountInput.tsx @@ -0,0 +1,54 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { BadgeDollarSign } from 'lucide-react'; +import { Label } from '@/components/ui/label'; +import InputWithIcon from '@/common/forms/components/InputWithIcon'; +import type { BettingControlsProps } from './BettingControls'; +import { BetAmountButton } from './BetAmountButton'; + +export function BetAmountInput({ + betAmount, + onBetAmountChange, + isInputDisabled, +}: Pick & { + isInputDisabled?: boolean; +}): JSX.Element { + const queryClient = useQueryClient(); + const balance = queryClient.getQueryData(['balance']) || 0; + return ( +
+ +
+
+ } + min={0} + onChange={e => { + onBetAmountChange?.(Number(e.target.value)); + }} + step={1} + type="number" + value={betAmount} + wrapperClassName="h-10 rounded-r-none rounded-none rounded-l" + /> +
+ { + onBetAmountChange?.(betAmount ?? 0, 0.5); + }} + /> + balance : true + } + label="2×" + onClick={() => { + onBetAmountChange?.(betAmount ?? 0, 2); + }} + /> +
+
+ ); +} diff --git a/apps/frontend/src/features/games/common/components/BettingControls.tsx b/apps/frontend/src/features/games/common/components/BettingControls.tsx new file mode 100644 index 0000000..9b83847 --- /dev/null +++ b/apps/frontend/src/features/games/common/components/BettingControls.tsx @@ -0,0 +1,105 @@ +import { BadgeDollarSign } from 'lucide-react'; +import { useQueryClient } from '@tanstack/react-query'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import InputWithIcon from '@/common/forms/components/InputWithIcon'; +import { BetAmountInput } from './BetAmountInput'; + +export interface BettingControlsProps { + betAmount?: number; + profitOnWin?: number; + isPending?: boolean; + onBetAmountChange?: (amount: number, multiplier?: number) => void; + onBet?: () => Promise; + betButtonText?: string; + icon?: React.ReactNode; +} + +function ProfitDisplay({ + profitOnWin, +}: Pick): JSX.Element { + return ( +
+ + } + value={profitOnWin} + wrapperClassName="bg-input-disabled shadow-md" + /> +
+ ); +} + +export function BetButton({ + isPending, + disabled, + onClick, + loadingImage, + betButtonText, + icon, + animate = 'animate-spin', +}: Pick & { + disabled: boolean; + onClick: () => void; +} & { + loadingImage: string; + betButtonText?: string; + icon?: React.ReactNode; + animate?: string; +}): JSX.Element { + return ( + + ); +} + +export function BettingControls({ + betAmount, + profitOnWin, + isPending, + onBetAmountChange, + onBet, + betButtonText, + icon, +}: BettingControlsProps): JSX.Element { + const queryClient = useQueryClient(); + const balance = queryClient.getQueryData(['balance']); + const isDisabled = + (betAmount ?? 0) > (balance ?? 0) || (betAmount ?? 0) <= 0 || isPending; + + return ( +
+ + + void onBet?.()} + /> +
+ ); +} diff --git a/apps/frontend/src/features/games/common/components/fairness-modal/ActiveSeeds.tsx b/apps/frontend/src/features/games/common/components/fairness-modal/ActiveSeeds.tsx new file mode 100644 index 0000000..9cfe5e0 --- /dev/null +++ b/apps/frontend/src/features/games/common/components/fairness-modal/ActiveSeeds.tsx @@ -0,0 +1,73 @@ +import { CopyIcon } from 'lucide-react'; +import type { ProvablyFairStateResponse } from '@repo/common/types'; +import InputWithIcon from '@/common/forms/components/InputWithIcon'; +import { Label } from '@/components/ui/label'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +function ActiveSeeds({ + activeSeeds, + isLoading, +}: { + activeSeeds: ProvablyFairStateResponse | null; + isLoading: boolean; +}): JSX.Element { + const activeSeedInputs = [ + { + key: 'clientSeed', + label: 'Active Client Seed', + value: activeSeeds?.clientSeed, + isCopyActive: true, + }, + { + key: 'hashedServerSeed', + label: 'Active Server Seed (Hashed)', + value: activeSeeds?.hashedServerSeed, + isCopyActive: true, + }, + { + key: 'nonce', + label: 'Total bets made with this pair', + value: activeSeeds?.nonce, + isCopyActive: false, + }, + ]; + + return ( +
+ {activeSeedInputs.map(input => ( +
+ +
+
+ +
+ {input.isCopyActive ? ( + + ) : null} +
+
+ ))} +
+ ); +} + +export default ActiveSeeds; diff --git a/apps/frontend/src/features/games/common/components/fairness-modal/RotateSeedPair.tsx b/apps/frontend/src/features/games/common/components/fairness-modal/RotateSeedPair.tsx new file mode 100644 index 0000000..b862d7c --- /dev/null +++ b/apps/frontend/src/features/games/common/components/fairness-modal/RotateSeedPair.tsx @@ -0,0 +1,90 @@ +import type { ProvablyFairStateResponse } from '@repo/common/types'; +import { useState } from 'react'; +import { CopyIcon } from 'lucide-react'; +import InputWithIcon from '@/common/forms/components/InputWithIcon'; +import { Label } from '@/components/ui/label'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { generateRandomString } from '@/lib/crypto'; + +interface RotateSeedPairProps { + rotateSeedPair: (clientSeed: string) => void; + activeSeeds: ProvablyFairStateResponse | null; + isLoading: boolean; +} + +function RotateSeedPair({ + rotateSeedPair, + activeSeeds, + isLoading, +}: RotateSeedPairProps): JSX.Element { + const [nextClientSeed, setNextClientSeed] = useState( + generateRandomString(10) + ); + + return ( +
+
Rotate Seed Pair
+
+ +
+
+ { + setNextClientSeed(e.target.value); + }} + value={nextClientSeed} + wrapperClassName={cn( + 'bg-brand-stronger border-brand-weaker shadow-none w-full pr-0 h-8 rounded-r-none' + )} + /> +
+ + +
+
+
+ +
+
+ +
+ + +
+
+
+ ); +} + +export default RotateSeedPair; diff --git a/apps/frontend/src/features/games/common/components/fairness-modal/Seeds.tsx b/apps/frontend/src/features/games/common/components/fairness-modal/Seeds.tsx new file mode 100644 index 0000000..496dab9 --- /dev/null +++ b/apps/frontend/src/features/games/common/components/fairness-modal/Seeds.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import type { ProvablyFairStateResponse } from '@repo/common/types'; +import { fetchActiveSeeds, fetchRotateSeedPair } from '@/api/user'; +import ActiveSeeds from './ActiveSeeds'; +import RotateSeedPair from './RotateSeedPair'; + +function Seeds({ isEnabled }: { isEnabled: boolean }): JSX.Element { + const [activeSeeds, setActiveSeeds] = + useState(null); + const { isPending } = useQuery({ + queryKey: ['active-seeds'], + queryFn: async () => { + const apiResponse = await fetchActiveSeeds(); + setActiveSeeds(apiResponse.data); + return apiResponse.data; + }, + enabled: isEnabled, + }); + + const { mutate: rotateSeedPair, isPending: isRotating } = useMutation({ + mutationFn: async (clientSeed: string) => { + const apiResponse = await fetchRotateSeedPair(clientSeed); + return apiResponse.data; + }, + onSuccess: data => { + setActiveSeeds(data); + }, + }); + + return ( +
+ + +
+ ); +} + +export default Seeds; diff --git a/apps/frontend/src/features/games/common/components/fairness-modal/VerificationInputs.tsx b/apps/frontend/src/features/games/common/components/fairness-modal/VerificationInputs.tsx new file mode 100644 index 0000000..47fa570 --- /dev/null +++ b/apps/frontend/src/features/games/common/components/fairness-modal/VerificationInputs.tsx @@ -0,0 +1,197 @@ +import { useEffect, useState } from 'react'; +import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; +import { Link, useLocation } from '@tanstack/react-router'; +import { Label } from '@/components/ui/label'; +import InputWithIcon from '@/common/forms/components/InputWithIcon'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import type { GameMeta } from '@/lib/verificationOutcomes'; +import { getVerificationOutcome } from '@/lib/verificationOutcomes'; +import { Games, type Game } from '@/const/games'; +import CommonSelect from '@/components/ui/common-select'; + +export interface VerificationInputsState { + clientSeed: string; + serverSeed: string; + nonce: string; + meta?: GameMeta; +} + +function VerificationInputs({ + setOutcome, + onSetVerificationInputs, + game, +}: { + setOutcome: (outcome: string | number[] | null) => void; + onSetVerificationInputs?: (inputs: VerificationInputsState | null) => void; + game: Game; +}): JSX.Element { + const { pathname } = useLocation(); + + const [meta, setMeta] = useState(null); + + const [verificationInputs, setVerificationInputs] = + useState({ + clientSeed: '', + serverSeed: '', + nonce: '0', + }); + + const handleInputChange = ( + input: keyof VerificationInputsState, + value: string + ): void => { + setVerificationInputs(prev => ({ ...prev, [input]: value })); + }; + + const incrementNonce = (): void => { + setVerificationInputs(prev => ({ + ...prev, + nonce: String(Number(prev.nonce) + 1), + })); + }; + + const decrementNonce = (): void => { + setVerificationInputs(prev => ({ + ...prev, + nonce: Math.max(0, Number(prev.nonce) - 1).toString(), + })); + }; + + const getGameMeta = (): JSX.Element | null => { + switch (game) { + case Games.MINES: + return ( + { + setMeta({ minesCount: Number(value) }); + setVerificationInputs(prev => ({ + ...prev, + meta: { minesCount: Number(value) }, + })); + }} + options={Array.from({ length: 24 }, (_, i) => ({ + label: (i + 1).toString(), + value: (i + 1).toString(), + }))} + value={meta?.minesCount.toString() ?? '3'} + /> + ); + default: + return null; + } + }; + + useEffect(() => { + const { clientSeed, serverSeed, nonce } = verificationInputs; + if (!clientSeed || !serverSeed) { + setOutcome(null); + onSetVerificationInputs?.(null); + return; + } + + void (async () => { + try { + const outcome = await getVerificationOutcome({ + game, + clientSeed, + serverSeed, + nonce, + ...(meta ? { meta } : {}), + }); + setOutcome(outcome); + onSetVerificationInputs?.(verificationInputs); + } catch (error: unknown) { + return error; + } + })(); + }, [verificationInputs, setOutcome, onSetVerificationInputs, game, meta]); + + return ( +
+
+ +
+ { + handleInputChange('clientSeed', e.target.value); + }} + value={verificationInputs.clientSeed} + wrapperClassName={cn( + 'bg-brand-stonger h-8 border-brand-weaker shadow-none w-full pr-0 ' + )} + /> +
+
+
+ +
+ { + handleInputChange('serverSeed', e.target.value); + }} + value={verificationInputs.serverSeed} + wrapperClassName={cn( + 'bg-brand-stronger h-8 border-brand-weaker shadow-none w-full pr-0 ' + )} + /> +
+
+
+ +
+
+ { + handleInputChange('nonce', e.target.value); + }} + step={1} + type="number" + value={verificationInputs.nonce} + wrapperClassName={cn( + 'bg-brand-stronger h-8 border-brand-weaker shadow-none w-full pr-0 rounded-r-none' + )} + /> +
+ + + +
+
+
{getGameMeta()}
+ {!pathname.includes('/provably-fair/calculation') && ( + +

+ View calculation breakdown +

+ + )} +
+ ); +} + +export default VerificationInputs; diff --git a/apps/frontend/src/features/games/common/components/fairness-modal/VerificationResult.tsx b/apps/frontend/src/features/games/common/components/fairness-modal/VerificationResult.tsx new file mode 100644 index 0000000..26c6105 --- /dev/null +++ b/apps/frontend/src/features/games/common/components/fairness-modal/VerificationResult.tsx @@ -0,0 +1,93 @@ +import { HashLoader } from 'react-spinners'; +import { NO_OF_TILES } from '@repo/common/game-utils/mines/constants.js'; +import { NO_OF_TILES_KENO } from '@repo/common/game-utils/keno/constants.js'; +import DiceResultPreview from '@/features/games/dice/components/DiceResultPreview'; +import { Games, type Game } from '@/const/games'; +import RouletteWheel from '@/features/games/roulette/components/RouletteWheel'; +import InactiveGameTile from '@/features/games/mines/components/InactiveGameTile'; +import VerificationResultKenoTile from '@/features/games/keno/components/VerificationResultKenoTile'; + +function VerificationResult({ + game, + outcome, +}: { + game: Game | null; + outcome: string | number[] | null; +}): JSX.Element | null { + const getResult = (): JSX.Element => { + switch (game) { + case Games.DICE: + return ; + case Games.ROULETTE: + return ( +
+
+ +
+
+ {outcome} +
+
+ ); + case Games.MINES: { + if (typeof outcome === 'string' || !outcome) return <>{null}; + return ( +
+ {Array.from({ length: NO_OF_TILES }, (_, i) => i).map(number => ( + + ))} +
+ ); + } + case Games.KENO: { + if (typeof outcome === 'string' || !outcome) return <>{null}; + return ( +
+ {Array.from({ length: NO_OF_TILES_KENO }, (_, i) => i).map( + number => ( + + ) + )} +
+ ); + } + + default: + return
Unknown game
; + } + }; + + return ( +
+ {outcome ? ( + getResult() + ) : ( +
+

+ More inputs are required to verify result +

+ +
+ )} +
+ ); +} + +export default VerificationResult; diff --git a/apps/frontend/src/features/games/common/components/fairness-modal/Verify.tsx b/apps/frontend/src/features/games/common/components/fairness-modal/Verify.tsx new file mode 100644 index 0000000..863d7cf --- /dev/null +++ b/apps/frontend/src/features/games/common/components/fairness-modal/Verify.tsx @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import CommonSelect from '@/components/ui/common-select'; +import { GAMES_DROPDOWN_OPTIONS, type Game } from '@/const/games'; +import VerificationResult from './VerificationResult'; +import VerificationInputs from './VerificationInputs'; + +function Verify({ game }: { game: Game }): JSX.Element { + const [outcome, setOutcome] = useState(null); + const [selectedGame, setSelectedGame] = useState(game); + + return ( + <> +
+ +
+
+ { + setSelectedGame(value as Game); + }} + options={GAMES_DROPDOWN_OPTIONS} + value={selectedGame} + /> + +
+ + ); +} + +export default Verify; diff --git a/apps/frontend/src/features/games/common/components/fairness-modal/index.tsx b/apps/frontend/src/features/games/common/components/fairness-modal/index.tsx new file mode 100644 index 0000000..6bdaf40 --- /dev/null +++ b/apps/frontend/src/features/games/common/components/fairness-modal/index.tsx @@ -0,0 +1,59 @@ +import { ScaleIcon } from 'lucide-react'; +import { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Games, type Game } from '@/const/games'; +import { cn } from '@/lib/utils'; +import Seeds from './Seeds'; +import Verify from './Verify'; + +export function FairnessModal({ game }: { game: Game }): JSX.Element { + const [open, setOpen] = useState(false); + const [activeTab, setActiveTab] = useState<'seeds' | 'verify'>('seeds'); + + return ( + + +

+ Fairness +

+
+ + + + + Fairness + + + { + setActiveTab(value as 'seeds' | 'verify'); + }} + > + + Seeds + Verify + + + + + + + + + + +
+ ); +} diff --git a/apps/frontend/src/features/games/common/components/game-settings/index.tsx b/apps/frontend/src/features/games/common/components/game-settings/index.tsx new file mode 100644 index 0000000..e85cfef --- /dev/null +++ b/apps/frontend/src/features/games/common/components/game-settings/index.tsx @@ -0,0 +1,22 @@ +import { SettingsIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import CommonTooltip from '@/components/ui/common-tooltip'; +import type { Game } from '@/const/games'; +import { FairnessModal } from '../fairness-modal'; + +function GameSettingsBar({ game }: { game: Game }): JSX.Element { + return ( +
+
+ Game Settings

}> + +
+
+ +
+ ); +} + +export default GameSettingsBar; diff --git a/apps/frontend/src/features/games/dice/components/DiceGameControls.tsx b/apps/frontend/src/features/games/dice/components/DiceGameControls.tsx new file mode 100644 index 0000000..94a98a2 --- /dev/null +++ b/apps/frontend/src/features/games/dice/components/DiceGameControls.tsx @@ -0,0 +1,133 @@ +import InputWithIcon from '@/common/forms/components/InputWithIcon'; +import { Label } from '@/components/ui/label'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import type { + GameControl, + NumericControl, + RollControl, +} from '../config/controls'; +import type { DiceStore } from '../store/diceStore'; + +interface GameControlsProps { + controls: GameControl[]; + state: DiceStore; +} + +function NumericControlInput({ + control, + state, +}: { + control: NumericControl; + state: DiceStore; +}): JSX.Element { + const value = control.getValue(state); + const isValid = value >= control.min && value <= control.max; + const Icon = control.icon; + + return ( +
+ + + + {!isValid && ( + + } + min={control.min} + onChange={e => { + control.setValue(state, Number(e.target.value)); + }} + step={control.step} + type="number" + value={value} + wrapperClassName="border-red-500 hover:border-red-500" + /> + + )} + +

{control.getValidationMessage(value)}

+
+
+
+ {isValid ? ( + } + min={control.min} + onChange={e => { + control.setValue(state, Number(e.target.value)); + }} + step={control.step} + type="number" + value={value} + /> + ) : null} +
+ ); +} + +function RollControlInput({ + control, + state, +}: { + control: RollControl; + state: DiceStore; +}): JSX.Element { + const value = control.getValue(state); + const condition = control.getCondition(state); + const Icon = control.icon; + + return ( +
+ +
{ + control.onToggle(state); + }} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + control.onToggle(state); + } + }} + role="button" + tabIndex={0} + > + {value} + +
+
+ ); +} + +function ControlInput({ + control, + state, +}: { + control: GameControl; + state: DiceStore; +}): JSX.Element { + if (control.type === 'numeric') { + return ; + } + return ; +} + +export function DiceGameControls({ + controls, + state, +}: GameControlsProps): JSX.Element { + return ( +
+ {controls.map(control => ( + + ))} +
+ ); +} diff --git a/apps/frontend/src/features/games/dice/components/DiceResultBreakdown.tsx b/apps/frontend/src/features/games/dice/components/DiceResultBreakdown.tsx new file mode 100644 index 0000000..666fb14 --- /dev/null +++ b/apps/frontend/src/features/games/dice/components/DiceResultBreakdown.tsx @@ -0,0 +1,165 @@ +import { Fragment, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { HashLoader } from 'react-spinners'; +import { calculateFinalOutcome } from '@repo/common/game-utils/dice/utils.js'; +import { getGeneratedFloats, byteGenerator } from '@/lib/crypto'; +import { Label } from '@/components/ui/label'; +import { cn } from '@/lib/utils'; +// Simple function to +// generate a stable unique ID without using array indices +const generateUniqueId = ( + prefix: string, + ...parts: (string | number)[] +): string => { + return `${prefix}-${parts.join('-')}`; +}; + +interface DiceResultBreakdownProps { + nonce?: string; + serverSeed?: string; + clientSeed?: string; +} + +function DiceResultBreakdown({ + nonce, + serverSeed, + clientSeed, +}: DiceResultBreakdownProps): JSX.Element { + const { data: hmacArray = [] } = useQuery({ + queryKey: ['hmacBuffer', serverSeed, clientSeed, nonce], + queryFn: async () => { + const bytes = await byteGenerator( + serverSeed ?? '', + `${clientSeed}:${nonce}`, + 1 + ); + return bytes; + }, + }); + + const { data: outcome } = useQuery({ + queryKey: ['result', serverSeed, clientSeed, nonce], + queryFn: async () => { + const result = await getGeneratedFloats({ + count: 1, + seed: serverSeed ?? '', + message: `${clientSeed}:${nonce}`, + }); + return result[0]; + }, + }); + + const selectedBytes = hmacArray.slice(0, 4); + + // Create unique identifiers for each byte in the hmacArray + const hmacByteIds = useMemo(() => { + return hmacArray.map((byte, idx) => ({ + byte, + id: generateUniqueId('byte', byte, idx, Date.now()), + })); + }, [hmacArray]); + + // Create unique identifiers for selected bytes + const selectedByteIds = useMemo(() => { + return selectedBytes.map((byte, idx) => ({ + byte, + id: generateUniqueId('selected-byte', byte, idx, Date.now()), + })); + }, [selectedBytes]); + + if (!serverSeed || !clientSeed || !outcome) { + return ; + } + + const finalOutcome = calculateFinalOutcome(outcome); + + return ( +
+
+ +

{finalOutcome}

+
+
+ +

+ {`HMAC_SHA256(${serverSeed}, ${clientSeed}:${nonce}:0)`} +

+
+ {hmacByteIds.map(({ byte, id }, index) => ( +
+ {byte.toString(16).padStart(2, '0')} + {byte} +
+ ))} +
+
+
+ +
+
+
{`(${selectedBytes.join(', ')}) -> [0, ..., 10000] = ${String(outcome * 10001).split('.')[0]}`}
+ {selectedByteIds.map(({ byte, id }, index) => ( + + + {index > 0 ? '+' : ''} + + + {(byte / 256 ** (index + 1)) + .toFixed(12) + .split('') + .map((char, charIndex) => ( +
+ {char} +
+ ))} +
+ + {`(${Array((3 - (byte.toString().length % 3)) % 3) + .fill('0') + .join('')}${byte} / (256 ^ ${index + 1}))`} + +
+ ))} + = + + {outcome + .toFixed(12) + .split('') + .map((char, charIndex) => ( +
+ {char} +
+ ))} +
+ + (× 10001) + + = + + {String(outcome * 10001).split('.')[0]} + + .{String(outcome * 10001).split('.')[1]} + + +
+
+
+
+ ); +} + +export default DiceResultBreakdown; diff --git a/apps/frontend/src/features/games/dice/components/DiceResultPillsCarousel.tsx b/apps/frontend/src/features/games/dice/components/DiceResultPillsCarousel.tsx new file mode 100644 index 0000000..269a53e --- /dev/null +++ b/apps/frontend/src/features/games/dice/components/DiceResultPillsCarousel.tsx @@ -0,0 +1,36 @@ +import type { DicePlaceBetResponse } from '@repo/common/game-utils/dice/types.js'; +import { cn } from '@/lib/utils'; + +interface DiceResultPillsCarouselProps { + results: DicePlaceBetResponse[]; +} + +export function DiceResultPillsCarousel({ + results, +}: DiceResultPillsCarouselProps): JSX.Element { + const getAnimationClass = (index: number, resultsLength: number): string => { + if (resultsLength <= 5) return 'animate-slideInLeft'; + return index === 0 + ? 'animate-slideOutLeft opacity-0' + : 'animate-slideInLeft'; + }; + + return ( +
+ {results.map(({ id, payoutMultiplier, state }, index) => ( + 0 + ? 'bg-[#00e600] text-black' + : 'bg-secondary-light' + )} + key={id} + > + {state.result} + + ))} +
+ ); +} diff --git a/apps/frontend/src/features/games/dice/components/DiceResultPreview.tsx b/apps/frontend/src/features/games/dice/components/DiceResultPreview.tsx new file mode 100644 index 0000000..a6981ce --- /dev/null +++ b/apps/frontend/src/features/games/dice/components/DiceResultPreview.tsx @@ -0,0 +1,24 @@ +import { Slider } from '@/components/ui/dice-slider'; +import { Slider as ResultSlider } from '@/components/ui/dice-result-slider'; + +function DiceResultPreview({ result }: { result: number }): JSX.Element { + return ( + <> +
+ {[0, 25, 50, 75, 100].map(value => ( +
+ {value} +
+ ))} +
+
+ +
+ +
+
+ + ); +} + +export default DiceResultPreview; diff --git a/apps/frontend/src/features/games/dice/components/DiceSlider.tsx b/apps/frontend/src/features/games/dice/components/DiceSlider.tsx new file mode 100644 index 0000000..a9cc4eb --- /dev/null +++ b/apps/frontend/src/features/games/dice/components/DiceSlider.tsx @@ -0,0 +1,50 @@ +import { Slider as ResultSlider } from '@/components/ui/dice-result-slider'; +import { Slider } from '@/components/ui/dice-slider'; +import useDiceStore from '../store/diceStore'; + +interface DiceSliderProps { + handleValueChange: (value: number[]) => void; + showResultSlider: boolean; +} + +function DiceSlider({ + handleValueChange, + showResultSlider, +}: DiceSliderProps): JSX.Element { + const { target, condition, results } = useDiceStore(); + return ( + <> +
+ {[0, 25, 50, 75, 100].map(value => ( +
+ {value} +
+ ))} +
+
+ + {showResultSlider && results.at(-1)?.state.result ? ( +
+ {(() => { + const lastResult = results.at(-1); + if (!lastResult) return null; + return ( + 0} + value={[lastResult.state.result]} + /> + ); + })()} +
+ ) : null} +
+ + ); +} + +export default DiceSlider; diff --git a/apps/frontend/src/features/games/dice/components/ResultPillCarousel.tsx b/apps/frontend/src/features/games/dice/components/ResultPillCarousel.tsx new file mode 100644 index 0000000..68940f5 --- /dev/null +++ b/apps/frontend/src/features/games/dice/components/ResultPillCarousel.tsx @@ -0,0 +1,5 @@ +function ResultPillCarousel(): JSX.Element { + return
ResultPillCarousel
; +} + +export default ResultPillCarousel; diff --git a/apps/frontend/src/features/games/dice/config/controls.ts b/apps/frontend/src/features/games/dice/config/controls.ts new file mode 100644 index 0000000..7a5244b --- /dev/null +++ b/apps/frontend/src/features/games/dice/config/controls.ts @@ -0,0 +1,77 @@ +import { Percent, RefreshCw, XIcon } from 'lucide-react'; +import type { DiceCondition } from '@repo/common/game-utils/dice/types.js'; +import type { DiceStore } from '../store/diceStore'; + +export interface BaseControl { + id: string; + label: string; +} + +export interface NumericControl extends BaseControl { + type: 'numeric'; + icon: typeof XIcon; + min: number; + max: number; + step: number; + getValue: (state: DiceStore) => number; + setValue: (state: DiceStore, value: number) => void; + getValidationMessage: (value: number) => string; +} + +export interface RollControl extends BaseControl { + type: 'roll'; + icon: typeof RefreshCw; + getValue: (state: DiceStore) => number; + getCondition: (state: DiceStore) => DiceCondition; + onToggle: (state: DiceStore) => void; +} + +export type GameControl = NumericControl | RollControl; + +export const diceGameControls: GameControl[] = [ + { + id: 'multiplier', + type: 'numeric', + label: 'Multiplier', + icon: XIcon, + min: 1.0102, + max: 9900, + step: 0.0001, + getValue: state => state.multiplier, + setValue: (state, value) => { + state.setMultiplier(value); + }, + getValidationMessage: value => + `${value < 1.0102 ? 'Minimum' : 'Maximum'} is ${ + value < 1.0102 ? '1.0102' : '9900' + }`, + }, + { + id: 'roll', + type: 'roll', + label: 'Roll', + icon: RefreshCw, + getValue: state => state.target, + getCondition: state => state.condition, + onToggle: state => { + state.toggleCondition(); + }, + }, + { + id: 'winChance', + type: 'numeric', + label: 'Win chance', + icon: Percent, + min: 0.01, + max: 98, + step: 0.01, + getValue: state => state.winChance, + setValue: (state, value) => { + state.setWinningChance(value); + }, + getValidationMessage: value => + `${value < 0.01 ? 'Minimum' : 'Maximum'} is ${ + value < 0.01 ? '0.01' : '98' + }`, + }, +]; diff --git a/apps/frontend/src/features/games/dice/hooks/useBetting.ts b/apps/frontend/src/features/games/dice/hooks/useBetting.ts new file mode 100644 index 0000000..c7ad502 --- /dev/null +++ b/apps/frontend/src/features/games/dice/hooks/useBetting.ts @@ -0,0 +1,49 @@ +import type { UseMutateFunction } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { + DicePlaceBetRequestBody, + DicePlaceBetResponse, +} from '@repo/common/game-utils/dice/types.js'; +import type { ApiResponse } from '@repo/common/types'; +import { placeBet } from '@/api/games/dice'; +import { useAudio } from '@/common/hooks/useAudio'; +import win from '@/assets/audio/win.mp3'; + +interface UseDiceBettingProps { + setResult: (result: DicePlaceBetResponse) => void; + setLastResultId: (id: string) => void; +} + +interface UseDiceBettingResult { + mutate: UseMutateFunction< + ApiResponse, + Error, + DicePlaceBetRequestBody + >; + isPending: boolean; +} + +export function useDiceBetting({ + setResult, + setLastResultId, +}: UseDiceBettingProps): UseDiceBettingResult { + const { play: playWinSound } = useAudio(win); + const queryClient = useQueryClient(); + + const { mutate, isPending } = useMutation({ + mutationFn: placeBet, + onSuccess: (response: ApiResponse) => { + setLastResultId(Date.now().toString()); + setResult(response.data); + queryClient.setQueryData(['balance'], () => response.data.balance); + if (response.data.payoutMultiplier > 0) { + void playWinSound(); + } + }, + }); + + return { + mutate, + isPending, + }; +} diff --git a/apps/frontend/src/features/games/dice/hooks/useDiceAudio.ts b/apps/frontend/src/features/games/dice/hooks/useDiceAudio.ts new file mode 100644 index 0000000..6ae5acb --- /dev/null +++ b/apps/frontend/src/features/games/dice/hooks/useDiceAudio.ts @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; +import bet from '@/assets/audio/bet.mp3'; +import rolling from '@/assets/audio/rolling.mp3'; +import { useAudio } from '@/common/hooks/useAudio'; + +export function useDiceAudio(isPending: boolean): { + playBetSound: () => Promise; +} { + const { play: playBetSound } = useAudio(bet); + const { playInfinite: playRollingSound, stop: stopRollingSound } = + useAudio(rolling); + + useEffect(() => { + if (isPending) { + playRollingSound(); + } else { + stopRollingSound(); + } + + return () => { + stopRollingSound(); + }; + }, [isPending, playRollingSound, stopRollingSound]); + + return { + playBetSound, + }; +} diff --git a/apps/frontend/src/features/games/dice/hooks/useResultSlider.ts b/apps/frontend/src/features/games/dice/hooks/useResultSlider.ts new file mode 100644 index 0000000..bd680c5 --- /dev/null +++ b/apps/frontend/src/features/games/dice/hooks/useResultSlider.ts @@ -0,0 +1,32 @@ +import { useCallback, useEffect, useState } from 'react'; + +export function useResultSlider(): { + showResultSlider: boolean; + setLastResultId: (id: string) => void; +} { + const [showResultSlider, setShowResultSlider] = useState(false); + const [lastResultId, setLastResultId] = useState(null); + + const startTimer = useCallback(() => { + setShowResultSlider(true); + const timer = setTimeout(() => { + setShowResultSlider(false); + }, 3000); + return timer; + }, []); + + useEffect(() => { + let timer: NodeJS.Timeout | null = null; + if (lastResultId) { + timer = startTimer(); + } + return () => { + if (timer) clearTimeout(timer); + }; + }, [lastResultId, startTimer]); + + return { + showResultSlider, + setLastResultId, + }; +} diff --git a/apps/frontend/src/features/games/dice/hooks/useSliderValue.ts b/apps/frontend/src/features/games/dice/hooks/useSliderValue.ts new file mode 100644 index 0000000..6bf728a --- /dev/null +++ b/apps/frontend/src/features/games/dice/hooks/useSliderValue.ts @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import { useAudio } from '@/common/hooks/useAudio'; +import tick from '@/assets/audio/tick.mp3'; +import useDiceStore from '../store/diceStore'; + +export function useSliderValue(): { + handleValueChange: (value: number[]) => void; +} { + const { setTarget } = useDiceStore(); + const [previousValue, setPreviousValue] = useState(null); + const { playThrottled: playTickSound } = useAudio(tick, 0.8); + + const handleValueChange = (value: number[]): void => { + const newValue = value[0]; + + if (previousValue === newValue) return; + + if (previousValue !== null) { + playSoundForRange(previousValue, newValue); + } + + setPreviousValue(newValue); + setTarget(newValue); + }; + + const playSoundForRange = (start: number, end: number): void => { + const lowerBound = Math.min(start, end); + const upperBound = Math.max(start, end); + + for (let i = lowerBound; i <= upperBound; i++) { + playTickSound(); + } + }; + + return { handleValueChange }; +} diff --git a/apps/frontend/src/features/games/dice/index.tsx b/apps/frontend/src/features/games/dice/index.tsx new file mode 100644 index 0000000..cd1e046 --- /dev/null +++ b/apps/frontend/src/features/games/dice/index.tsx @@ -0,0 +1,61 @@ +import useDiceStore from '@/features/games/dice/store/diceStore'; +import { Games } from '@/const/games'; +import { BettingControls } from '../common/components/BettingControls'; +import GameSettingsBar from '../common/components/game-settings'; +import { DiceResultPillsCarousel } from './components/DiceResultPillsCarousel'; +import { useDiceAudio } from './hooks/useDiceAudio'; +import { useDiceBetting } from './hooks/useBetting'; +import { useResultSlider } from './hooks/useResultSlider'; +import { useSliderValue } from './hooks/useSliderValue'; +import DiceSlider from './components/DiceSlider'; +import { DiceGameControls } from './components/DiceGameControls'; +import { diceGameControls } from './config/controls'; + +export function DiceGame(): JSX.Element { + const diceState = useDiceStore(); + const { betAmount, profitOnWin, results, setBetAmount, setResult } = + diceState; + const { playBetSound } = useDiceAudio(false); + const { showResultSlider, setLastResultId } = useResultSlider(); + const { handleValueChange } = useSliderValue(); + const { mutate, isPending } = useDiceBetting({ + setResult, + setLastResultId, + }); + + const handleBet = async (): Promise => { + await playBetSound(); + mutate({ + target: diceState.target, + condition: diceState.condition, + betAmount, + }); + }; + + return ( + <> +
+ { + setBetAmount(amount * multiplier); + }} + profitOnWin={profitOnWin} + /> +
+ +
+ +
+ +
+
+ + + ); +} diff --git a/apps/frontend/src/features/games/dice/store/diceStore.ts b/apps/frontend/src/features/games/dice/store/diceStore.ts new file mode 100644 index 0000000..df2eb67 --- /dev/null +++ b/apps/frontend/src/features/games/dice/store/diceStore.ts @@ -0,0 +1,126 @@ +import { + calculateMultiplier, + calculateProfit, + calculateTargetFromChance, + calculateTargetWithMultiplier, + calculateWinningChance, +} from '@repo/common/game-utils/dice/index.js'; +import type { + DiceCondition, + DicePlaceBetResponse, +} from '@repo/common/game-utils/dice/index.js'; +import { create } from 'zustand'; + +const initalTarget = 50.5; +const initialCondition: DiceCondition = 'above'; + +export interface DiceStore { + betAmount: number; + profitOnWin: number; + multiplier: number; + target: number; + winChance: number; + condition: DiceCondition; + results: DicePlaceBetResponse[]; + setTarget: (target: number) => void; + toggleCondition: () => void; + setMultiplier: (multiplier: number) => void; + setWinningChance: (winChance: number) => void; + setBetAmount: (betAmount: number) => void; + setResult: (result: DicePlaceBetResponse) => void; +} + +const useDiceStore = create(set => ({ + betAmount: 0, + profitOnWin: 0, + multiplier: calculateMultiplier(initalTarget, initialCondition), + target: initalTarget, + winChance: calculateWinningChance(initalTarget, initialCondition), + condition: initialCondition, + results: [], + + setTarget: (target: number) => { + const clampedTarget = Math.min(98, Math.max(2, target)); + set(state => { + const multiplier = calculateMultiplier(clampedTarget, state.condition); + return { + ...state, + target: clampedTarget, + multiplier, + winChance: calculateWinningChance(clampedTarget, state.condition), + profitOnWin: calculateProfit(multiplier, state.betAmount), + }; + }); + }, + + setBetAmount: (betAmount: number) => { + set(state => ({ + ...state, + betAmount, + profitOnWin: calculateProfit(state.multiplier, betAmount), + })); + }, + + toggleCondition: () => { + set(state => { + const target = 100 - state.target; + const condition = state.condition === 'above' ? 'below' : 'above'; + const multiplier = calculateMultiplier(target, condition); + return { + ...state, + condition, + multiplier, + winChance: calculateWinningChance(target, condition), + target, + profitOnWin: calculateProfit(multiplier, state.betAmount), + }; + }); + }, + + setMultiplier: (multiplier: number) => { + const clampedMultiplier = Math.min(9900, multiplier); + set(state => { + const target = calculateTargetWithMultiplier( + clampedMultiplier, + state.condition + ); + return { + ...state, + multiplier, + winChance: calculateWinningChance(target, state.condition), + target, + profitOnWin: calculateProfit(multiplier, state.betAmount), + }; + }); + }, + + setWinningChance: (winChance: number) => { + const clampedWinChance = Math.min(99.99, Math.max(0.01, winChance)); + set(state => { + const target = calculateTargetFromChance( + clampedWinChance, + state.condition + ); + const multiplier = calculateMultiplier(target, state.condition); + return { + ...state, + winChance: clampedWinChance, + target, + multiplier, + profitOnWin: calculateProfit(multiplier, state.betAmount), + }; + }); + }, + + setResult: (result: DicePlaceBetResponse) => { + set(state => { + const newResults = [...state.results, result]; + if (newResults.length > 6) { + newResults.shift(); + } + return { ...state, results: newResults }; + }); + }, +})); + +export default useDiceStore; diff --git a/apps/frontend/src/features/games/keno/components/BettingControls.tsx b/apps/frontend/src/features/games/keno/components/BettingControls.tsx new file mode 100644 index 0000000..aed2709 --- /dev/null +++ b/apps/frontend/src/features/games/keno/components/BettingControls.tsx @@ -0,0 +1,126 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { KenoRisk } from '@repo/common/game-utils/keno/types.js'; +import { NO_OF_TILES_KENO } from '@repo/common/game-utils/keno/constants.js'; +import CommonSelect from '@/components/ui/common-select'; +import { Button } from '@/components/ui/button'; +import { placeBet } from '@/api/games/keno'; +import { BetButton } from '../../common/components/BettingControls'; +import { BetAmountInput } from '../../common/components/BetAmountInput'; +import useKenoStore from '../store/kenoStore'; +import { KenoRiskDropdown } from '../const'; +import { useSelectedTiles } from '../store/kenoSelectors'; + +function BettingControls(): JSX.Element { + const { + betAmount, + setBetAmount, + kenoRisk, + setKenoRisk, + clearTiles, + updateSelectedTile, + setOutcome, + } = useKenoStore(); + + const selectedTiles = useSelectedTiles(); + + const { mutate: placeBetMutation, isPending } = useMutation({ + mutationKey: ['keno-start-game'], + mutationFn: () => + placeBet({ + betAmount, + selectedTiles: Array.from(selectedTiles), + risk: kenoRisk, + }), + onSuccess: async ({ data }): Promise => { + const drawnNumbers = data.state.drawnNumbers; + + const updatePromises = Array.from(drawnNumbers.keys()).map(index => + sleep(100 * index).then(() => { + setOutcome({ + ...data, + state: { + ...data.state, + drawnNumbers: drawnNumbers.slice(0, index + 1), + }, + }); + }) + ); + + await Promise.all(updatePromises); + }, + }); + const queryClient = useQueryClient(); + const balance = queryClient.getQueryData(['balance']); + const isDisabled = + betAmount > (balance ?? 0) || betAmount <= 0 || selectedTiles.size === 0; + + // async function to update selected tile and sleep for 200ms + const sleep = (ms: number): Promise => + new Promise(resolve => { + setTimeout(resolve, ms); + }); + + const autoPickTiles = async (): Promise => { + // Choose 10 random tiles between 1 and 40 + const randomTiles = new Set(); + while (randomTiles.size < 10) { + const randomTile = Math.floor(Math.random() * NO_OF_TILES_KENO) + 1; + randomTiles.add(randomTile); + } + + // Update tiles with delays + const tilesArray = Array.from(randomTiles); + const updatePromises = tilesArray.map((tile, index) => + sleep(index * 100).then(() => { + updateSelectedTile(tile); + }) + ); + + await Promise.all(updatePromises); + }; + + return ( +
+
+ { + setBetAmount(amount * multiplier); + }} + /> + { + setKenoRisk(value as KenoRisk); + }} + options={KenoRiskDropdown} + triggerClassName="h-10 text-sm font-medium bg-brand-stronger" + value={kenoRisk} + /> + + +
+ + +
+ ); +} + +export default BettingControls; diff --git a/apps/frontend/src/features/games/keno/components/KenoResultBreakdown.tsx b/apps/frontend/src/features/games/keno/components/KenoResultBreakdown.tsx new file mode 100644 index 0000000..75d0f1a --- /dev/null +++ b/apps/frontend/src/features/games/keno/components/KenoResultBreakdown.tsx @@ -0,0 +1,252 @@ +import { useQuery } from '@tanstack/react-query'; +import React, { Fragment, useMemo } from 'react'; +import { convertFloatsToGameEvents } from '@repo/common/game-utils/mines/utils.js'; +import { HashLoader } from 'react-spinners'; +import { chunk } from 'lodash'; +import { NO_OF_TILES_KENO } from '@repo/common/game-utils/keno/constants.js'; +import { calculateSelectedGems } from '@repo/common/game-utils/keno/utils.js'; +import { + byteGenerator, + getFisherYatesShuffle, + getGeneratedFloats, +} from '@/lib/crypto'; +import { cn } from '@/lib/utils'; +import { Label } from '@/components/ui/label'; + +const generateUniqueId = ( + prefix: string, + ...parts: (string | number)[] +): string => { + return `${prefix}-${parts.join('-')}`; +}; + +interface KenoResultBreakdownProps { + clientSeed?: string; + nonce?: string; + serverSeed?: string; +} +function KenoResultBreakdown({ + clientSeed, + nonce, + serverSeed, +}: KenoResultBreakdownProps): JSX.Element { + const { data: hmacArray = [] } = useQuery({ + queryKey: ['hmacBuffer', serverSeed, clientSeed, nonce], + queryFn: async () => { + const bytes = await byteGenerator( + serverSeed ?? '', + `${clientSeed}:${nonce}`, + 2 + ); + return bytes; + }, + }); + + const { data: floats } = useQuery({ + queryKey: ['result', serverSeed, clientSeed, nonce], + queryFn: async () => { + const result = await getGeneratedFloats({ + count: 10, + seed: serverSeed ?? '', + message: `${clientSeed}:${nonce}`, + }); + return result; + }, + }); + + const gameEvents = convertFloatsToGameEvents(floats, NO_OF_TILES_KENO); + + const drawnNumbers = calculateSelectedGems(gameEvents, 10); + const finalDrawnNumbers = drawnNumbers.map(num => num + 1); + + const fisherYatesShuffle = getFisherYatesShuffle({ + gameEvents, + stopCount: 10, + totalEventsPossible: NO_OF_TILES_KENO, + }); + + // Create unique identifiers for each byte in the hmacArray + const hmacByteIds = useMemo(() => { + return hmacArray.map((byte, idx) => ({ + byte, + id: generateUniqueId('byte', byte, idx, Date.now()), + })); + }, [hmacArray]); + + const chunkedHmacByteIds = chunk( + hmacByteIds, + Math.ceil(hmacByteIds.length / 2) + ); + + // Create unique identifiers for selected bytes + const selectedByteIds = useMemo(() => { + return hmacArray.map((byte, idx) => ({ + byte, + id: generateUniqueId('selected-byte', byte, idx, Date.now()), + })); + }, [hmacArray]); + + const chunkedSelectedByteIds = chunk(selectedByteIds, 4); + + if (!serverSeed || !clientSeed || !floats) { + return ; + } + + return ( +
+
+ +

+ ( {drawnNumbers.join(', ')} ) +

+
+ 1 =
+

+ + ( {finalDrawnNumbers.join(', ')} ) + +

+
+ +
+ +
+ {chunkedHmacByteIds.map((chunkedHmacByteId, index) => ( +
+

+ {`HMAC_SHA256(${serverSeed}, ${clientSeed}:${nonce}:${index})`} +

+
+ {chunkedHmacByteId.map(({ byte, id }, chunkIndex) => ( +
= 8 && index === 1, + } + )} + key={id} + > + {byte.toString(16).padStart(2, '0')} + {byte} +
+ ))} +
+
+ ))} +
+
+ +
+ +
+
+ {chunkedSelectedByteIds.slice(0, 10).map((selectedBytes, index) => { + return ( +
+
+
{`(${selectedBytes.map(({ byte }) => byte).join(', ')}) -> [0, ..., ${NO_OF_TILES_KENO - 1 - index}] = ${Math.floor(floats[index] * (NO_OF_TILES_KENO - index))}`}
+ {selectedBytes.map(({ byte, id }, i) => ( + + + {i > 0 ? '+' : ''} + + + {(byte / 256 ** (i + 1)) + .toFixed(12) + .split('') + .map((char, charIndex) => ( +
+ {char} +
+ ))} +
+ + {`(${Array((3 - (byte.toString().length % 3)) % 3) + .fill('0') + .join('')}${byte} / (256 ^ ${i + 1}))`} + +
+ ))} + = + + {floats[index] + .toFixed(12) + .split('') + .map((char, charIndex) => ( +
+ {char} +
+ ))} +
+ + (× {NO_OF_TILES_KENO - index}) + + = + + { + String( + (floats[index] * (NO_OF_TILES_KENO - index)).toFixed( + 12 + ) + ).split('.')[0] + } + + . + { + String( + ( + floats[index] * + (NO_OF_TILES_KENO - index) + ).toFixed(12) + ).split('.')[1] + } + + +
+
+ ); + })} +
+
+
+
+ +
+ {fisherYatesShuffle.map(({ array, chosenIndex }, index) => ( +
+
+ {array.map((byte, idx) => ( +
+ {byte} +
+ ))} +
+
+ ))} +
+
+
+ ); +} + +export default KenoResultBreakdown; diff --git a/apps/frontend/src/features/games/keno/components/KenoTile.tsx b/apps/frontend/src/features/games/keno/components/KenoTile.tsx new file mode 100644 index 0000000..6523a79 --- /dev/null +++ b/apps/frontend/src/features/games/keno/components/KenoTile.tsx @@ -0,0 +1,59 @@ +import { cn } from '@/lib/utils'; +import { useDrawnNumbers, useSelectedTiles } from '../store/kenoSelectors'; +import useKenoStore from '../store/kenoStore'; + +function KenoTile({ + isLoading, + index, +}: { + isLoading: boolean; + index: number; +}): JSX.Element { + const selectedTiles = useSelectedTiles(); + const drawnNumbers = useDrawnNumbers(); + + const { updateSelectedTile, outcome } = useKenoStore(); + + const isSelected = selectedTiles.has(index); + const isDrawn = drawnNumbers.has(index); + + const handleClick = (): void => { + if (isLoading) return; + updateSelectedTile(index); + }; + + const isTileDisabled = Boolean(outcome) && !isSelected; + + return ( +
{ + if (e.key === 'Enter' || e.key === ' ') { + handleClick(); + } + }} + role="button" + tabIndex={0} + > + {index} +
+ ); +} + +export default KenoTile; diff --git a/apps/frontend/src/features/games/keno/components/VerificationResultKenoTile.tsx b/apps/frontend/src/features/games/keno/components/VerificationResultKenoTile.tsx new file mode 100644 index 0000000..94ba17d --- /dev/null +++ b/apps/frontend/src/features/games/keno/components/VerificationResultKenoTile.tsx @@ -0,0 +1,28 @@ +import { cn } from '@/lib/utils'; + +function VerificationResultKenoTile({ + drawnNumbers, + index, +}: { + drawnNumbers: Set; + index: number; +}): JSX.Element { + const isDrawn = drawnNumbers.has(index); + + return ( +
+ {index} +
+ ); +} + +export default VerificationResultKenoTile; diff --git a/apps/frontend/src/features/games/keno/const/index.ts b/apps/frontend/src/features/games/keno/const/index.ts new file mode 100644 index 0000000..eaccc3d --- /dev/null +++ b/apps/frontend/src/features/games/keno/const/index.ts @@ -0,0 +1,6 @@ +export const KenoRiskDropdown = [ + { value: 'classic', label: 'Classic' }, + { value: 'low', label: 'Low' }, + { value: 'medium', label: 'Medium' }, + { value: 'high', label: 'High' }, +]; diff --git a/apps/frontend/src/features/games/keno/index.tsx b/apps/frontend/src/features/games/keno/index.tsx new file mode 100644 index 0000000..21c3b80 --- /dev/null +++ b/apps/frontend/src/features/games/keno/index.tsx @@ -0,0 +1,148 @@ +import { + KENO_PROBABILITY, + NO_OF_TILES_KENO, + PAYOUT_MULTIPLIERS, +} from '@repo/common/game-utils/keno/constants.js'; +import { BadgeDollarSign, BadgeDollarSignIcon } from 'lucide-react'; +import { Games } from '@/const/games'; +import { Label } from '@/components/ui/label'; +import GameSettingsBar from '../common/components/game-settings'; +import BettingControls from './components/BettingControls'; +import KenoTile from './components/KenoTile'; +import { + useDrawnNumbers, + usePayout, + usePayoutMultiplier, + useSelectedTiles, +} from './store/kenoSelectors'; +import useKenoStore from './store/kenoStore'; + +export function Keno(): JSX.Element { + const selectedTiles = useSelectedTiles(); + const { kenoRisk, hoveredTile, setHoveredTile, betAmount } = useKenoStore(); + const hoveredTilePayoutMultiplier = + hoveredTile !== null + ? PAYOUT_MULTIPLIERS[kenoRisk][selectedTiles.size][hoveredTile].toFixed(2) + : null; + + const payoutMultiplier = usePayoutMultiplier(); + const payout = usePayout(); + const drawnNumbers = useDrawnNumbers(); + + return ( + <> +
+ + +
+
+
+ {Array.from({ length: NO_OF_TILES_KENO }, (_, i) => i).map( + number => ( + + ) + )} +
+
+ {selectedTiles.size === 0 ? ( +
+ Select 1-10 numbers to play +
+ ) : ( +
+
+ {Array.from( + { length: selectedTiles.size + 1 }, + (_, i) => i + ).map(tile => ( +
+ {PAYOUT_MULTIPLIERS[kenoRisk][selectedTiles.size][ + tile + ].toFixed(2)} + x +
+ ))} +
+
+ {hoveredTile !== null && ( +
+
+ +
+ {hoveredTilePayoutMultiplier}x +
+
+
+ +
+ {( + Number(hoveredTilePayoutMultiplier) * betAmount + ).toFixed(8)} + +
+
+
+ +
+ {KENO_PROBABILITY[selectedTiles.size][hoveredTile]} +
+
+
+ )} + {Array.from( + { length: selectedTiles.size + 1 }, + (_, i) => i + ).map(tile => ( +
{ + setHoveredTile(tile); + }} + onMouseLeave={() => { + setHoveredTile(null); + }} + > + {tile}x{' '} + diamond +
+ ))} +
+
+ )} + + {payoutMultiplier > 0 && drawnNumbers.size === 10 ? ( +
+

+ {payoutMultiplier.toFixed(2)}x +

+
+

+ {payout || 0} + +

+
+ ) : null} +
+
+ + + ); +} diff --git a/apps/frontend/src/features/games/keno/store/kenoSelectors.ts b/apps/frontend/src/features/games/keno/store/kenoSelectors.ts new file mode 100644 index 0000000..7ff0632 --- /dev/null +++ b/apps/frontend/src/features/games/keno/store/kenoSelectors.ts @@ -0,0 +1,21 @@ +import useKenoStore from './kenoStore'; + +export const useSelectedTiles = (): Set => + useKenoStore(state => { + return new Set(state.selectedTiles); + }); + +export const useDrawnNumbers = (): Set => + useKenoStore(state => { + return new Set(state.outcome?.state.drawnNumbers ?? []); + }); + +export const usePayoutMultiplier = (): number => + useKenoStore(state => { + return state.outcome?.payoutMultiplier ?? 0; + }); + +export const usePayout = (): number => + useKenoStore(state => { + return state.outcome?.payout ?? 0; + }); diff --git a/apps/frontend/src/features/games/keno/store/kenoStore.ts b/apps/frontend/src/features/games/keno/store/kenoStore.ts new file mode 100644 index 0000000..5e89973 --- /dev/null +++ b/apps/frontend/src/features/games/keno/store/kenoStore.ts @@ -0,0 +1,60 @@ +import { create } from 'zustand'; +import type { + KenoResponse, + KenoRisk, +} from '@repo/common/game-utils/keno/types.js'; + +interface KenoStore { + betAmount: number; + setBetAmount: (betAmount: number) => void; + kenoRisk: KenoRisk; + setKenoRisk: (kenoRisk: KenoRisk) => void; + selectedTiles: number[]; + updateSelectedTile: (selectedTile: number) => void; + clearTiles: () => void; + hoveredTile: number | null; + setHoveredTile: (hoveredTile: number | null) => void; + outcome: null | KenoResponse; + setOutcome: (outcome: KenoResponse | null) => void; +} + +const useKenoStore = create(set => ({ + betAmount: 0, + setBetAmount: betAmount => { + set({ betAmount }); + }, + kenoRisk: 'classic', + setKenoRisk: kenoRisk => { + set({ kenoRisk }); + }, + selectedTiles: [], + clearTiles: () => { + set({ selectedTiles: [], outcome: null }); + }, + updateSelectedTile: (selectedTile: number) => { + set(state => { + if ( + state.selectedTiles.length >= 10 && + !state.selectedTiles.includes(selectedTile) + ) { + return state; + } + return { + selectedTiles: state.selectedTiles.includes(selectedTile) + ? state.selectedTiles.filter(t => t !== selectedTile) + : [...state.selectedTiles, selectedTile], + outcome: null, + }; + }); + }, + hoveredTile: null, + setHoveredTile: (hoveredTile: number | null) => { + set({ hoveredTile }); + }, + outcome: null, + setOutcome: (outcome: KenoResponse | null) => { + set({ outcome }); + }, +})); + +export default useKenoStore; diff --git a/apps/frontend/src/features/games/mines/components/ActiveGameTile.tsx b/apps/frontend/src/features/games/mines/components/ActiveGameTile.tsx new file mode 100644 index 0000000..d2209a1 --- /dev/null +++ b/apps/frontend/src/features/games/mines/components/ActiveGameTile.tsx @@ -0,0 +1,56 @@ +import { motion } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useSelectedTiles } from '../store/minesSelectors'; + +function ActiveGameTile({ + onClick, + isLoading, + index, +}: { + isLoading: boolean; + onClick: () => void; + index: number; +}): JSX.Element { + const selectedTiles = useSelectedTiles(); + const hasDiamond = selectedTiles?.has(index); + + return ( + + {hasDiamond ? ( + diamond + ) : null} + + ); +} + +export default ActiveGameTile; diff --git a/apps/frontend/src/features/games/mines/components/BettingControls.tsx b/apps/frontend/src/features/games/mines/components/BettingControls.tsx new file mode 100644 index 0000000..db8df06 --- /dev/null +++ b/apps/frontend/src/features/games/mines/components/BettingControls.tsx @@ -0,0 +1,180 @@ +import React, { useEffect } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { NO_OF_TILES } from '@repo/common/game-utils/mines/constants.js'; +import { startGame, getActiveGame, cashOut } from '@/api/games/mines'; +import CommonSelect from '@/components/ui/common-select'; +import { Label } from '@/components/ui/label'; +import InputWithIcon from '@/common/forms/components/InputWithIcon'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { BetButton } from '../../common/components/BettingControls'; +import { BetAmountInput } from '../../common/components/BetAmountInput'; +import useMinesStore from '../store/minesStore'; +import { useIsGameActive, useLastRound } from '../store/minesSelectors'; + +function BettingControls(): JSX.Element { + const { betAmount, setBetAmount, minesCount, setMinesCount, setGameState } = + useMinesStore(); + + const { + isPending: isFetchingActiveGame, + data: activeGame, + isError, + } = useQuery({ + queryKey: ['mines-active-game'], + queryFn: getActiveGame, + retry: false, + }); + + const { mutate: cashout, isPending: isCashingOut } = useMutation({ + mutationKey: ['mines-cashout'], + mutationFn: cashOut, + onSuccess: ({ data }) => { + setGameState(data); + }, + }); + + const { mutate: start, isPending: isStartingGame } = useMutation({ + mutationKey: ['mines-start-game'], + mutationFn: () => startGame({ betAmount, minesCount }), + onSuccess: ({ data }) => { + setGameState(data); + setBetAmount(Number(data.betAmount)); + }, + }); + const queryClient = useQueryClient(); + const balance = queryClient.getQueryData(['balance']); + const isDisabled = betAmount > (balance ?? 0) || betAmount <= 0; + + const isGameActive = useIsGameActive(); + const lastRound = useLastRound(); + useEffect(() => { + if (isError) { + setGameState(null); + setBetAmount(0); + return; + } + if (activeGame) { + setGameState(activeGame.data || null); + setBetAmount(Number(activeGame.data?.betAmount || 0)); + } + }, [activeGame, isError, setGameState, setBetAmount]); + + return ( +
+
+ { + setBetAmount(amount * multiplier); + }} + /> + {isGameActive ? ( +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+ +
+ ) : ( + { + setMinesCount(Number(value)); + }} + options={Array.from({ length: 24 }, (_, i) => ({ + label: (i + 1).toString(), + value: (i + 1).toString(), + }))} + triggerClassName="h-10 text-sm font-medium bg-brand-stronger" + value={minesCount.toString()} + /> + )} +
+ + {isGameActive ? ( + { + cashout(); + }} + /> + ) : ( + + )} +
+ ); +} + +export default BettingControls; diff --git a/apps/frontend/src/features/games/mines/components/InactiveGameTile.tsx b/apps/frontend/src/features/games/mines/components/InactiveGameTile.tsx new file mode 100644 index 0000000..e43509b --- /dev/null +++ b/apps/frontend/src/features/games/mines/components/InactiveGameTile.tsx @@ -0,0 +1,118 @@ +import type { MinesRound } from '@repo/common/game-utils/mines/types.js'; +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface InactiveGameTileProps { + index: number; + isGameLost: boolean; + mines: Set | null; + selectedTiles?: Set | null; + lastRound?: MinesRound | null; + isPreview?: boolean; + className?: string; +} +function InactiveGameTile({ + index, + isGameLost, + mines, + selectedTiles, + lastRound, + isPreview = false, + className = '', +}: InactiveGameTileProps): JSX.Element { + const renderTile = (): JSX.Element => { + if (isPreview) { + if (mines?.has(index)) + return ( + diamond + ); + return ( + diamond + ); + } + if (isGameLost) { + if (index === lastRound?.selectedTileIndex) + return ( + <> + diamond + diamond + + ); + if (mines?.has(index)) + return ( + diamond + ); + if (selectedTiles?.has(index)) + return ( + diamond + ); + return ( + diamond + ); + } + if (selectedTiles?.has(index)) + return ( + diamond + ); + if (mines?.has(index)) + return ( + diamond + ); + return ( + diamond + ); + }; + return ( +
+ {renderTile()} +
+ ); +} + +export default InactiveGameTile; diff --git a/apps/frontend/src/features/games/mines/components/MinesResultBreakdown.tsx b/apps/frontend/src/features/games/mines/components/MinesResultBreakdown.tsx new file mode 100644 index 0000000..147b1e1 --- /dev/null +++ b/apps/frontend/src/features/games/mines/components/MinesResultBreakdown.tsx @@ -0,0 +1,252 @@ +import { useQuery } from '@tanstack/react-query'; +import React, { Fragment, useMemo } from 'react'; +import { NO_OF_TILES } from '@repo/common/game-utils/mines/constants.js'; +import { + calculateMines, + convertFloatsToGameEvents, +} from '@repo/common/game-utils/mines/utils.js'; +import { HashLoader } from 'react-spinners'; +import { chunk } from 'lodash'; +import { + byteGenerator, + getFisherYatesShuffle, + getGeneratedFloats, +} from '@/lib/crypto'; +import { cn } from '@/lib/utils'; +import { Label } from '@/components/ui/label'; + +const generateUniqueId = ( + prefix: string, + ...parts: (string | number)[] +): string => { + return `${prefix}-${parts.join('-')}`; +}; + +interface MinesResultBreakdownProps { + clientSeed?: string; + nonce?: string; + serverSeed?: string; + minesCount?: number; +} +function MinesResultBreakdown({ + clientSeed, + nonce, + serverSeed, + minesCount, +}: MinesResultBreakdownProps): JSX.Element { + const { data: hmacArray = [] } = useQuery({ + queryKey: ['hmacBuffer', serverSeed, clientSeed, nonce, minesCount], + queryFn: async () => { + const bytes = await byteGenerator( + serverSeed ?? '', + `${clientSeed}:${nonce}`, + 3 + ); + return bytes; + }, + }); + + const { data: floats } = useQuery({ + queryKey: ['result', serverSeed, clientSeed, nonce, minesCount], + queryFn: async () => { + const result = await getGeneratedFloats({ + count: NO_OF_TILES - 1, + seed: serverSeed ?? '', + message: `${clientSeed}:${nonce}`, + }); + return result; + }, + }); + + const gameEvents = convertFloatsToGameEvents(floats, NO_OF_TILES); + + const allMines = calculateMines(gameEvents, NO_OF_TILES - 1); + + const mines = allMines.slice(0, minesCount ?? 3); + + const fisherYatesShuffle = getFisherYatesShuffle({ + gameEvents, + stopCount: NO_OF_TILES - 1, + totalEventsPossible: NO_OF_TILES, + }); + + // Create unique identifiers for each byte in the hmacArray + const hmacByteIds = useMemo(() => { + return hmacArray.map((byte, idx) => ({ + byte, + id: generateUniqueId('byte', byte, idx, Date.now()), + })); + }, [hmacArray]); + + const chunkedHmacByteIds = chunk( + hmacByteIds, + Math.ceil(hmacByteIds.length / 3) + ); + // Create unique identifiers for selected bytes + const selectedByteIds = useMemo(() => { + return hmacArray.map((byte, idx) => ({ + byte, + id: generateUniqueId('selected-byte', byte, idx, Date.now()), + })); + }, [hmacArray]); + + const chunkedSelectedByteIds = chunk(selectedByteIds, 4); + + if (!serverSeed || !clientSeed || !floats) { + return ; + } + + return ( +
+
+ +

+ Values:{' '} + {mines.join(', ')} +

+
+
+

x = (value mod 5) + 1

+

y = 5 - floor(value / 5)

+

(x, y) starts from bottom left

+
+
+ Mines Coordinates: + + {mines + .map(mine => `(${(mine % 5) + 1}, ${5 - Math.floor(mine / 5)})`) + .join(', ')} + +
+
+ +
+ {chunkedHmacByteIds.map((chunkedHmacByteId, index) => ( +
+

+ {`HMAC_SHA256(${serverSeed}, ${clientSeed}:${nonce}:${index})`} +

+
+ {chunkedHmacByteId.map(({ byte, id }) => ( +
+ {byte.toString(16).padStart(2, '0')} + {byte} +
+ ))} +
+
+ ))} +
+
+
+ +
+ {chunkedSelectedByteIds.map((selectedBytes, index) => { + return ( +
+
+
{`(${selectedBytes.map(({ byte }) => byte).join(', ')}) -> [0, ..., ${NO_OF_TILES - 1 - index}] = ${Math.floor(floats[index] * (NO_OF_TILES - index))}`}
+ {selectedBytes.map(({ byte, id }, i) => ( + + + {i > 0 ? '+' : ''} + + + {(byte / 256 ** (i + 1)) + .toFixed(12) + .split('') + .map((char, charIndex) => ( +
+ {char} +
+ ))} +
+ + {`(${Array((3 - (byte.toString().length % 3)) % 3) + .fill('0') + .join('')}${byte} / (256 ^ ${i + 1}))`} + +
+ ))} + = + + {floats[index] + .toFixed(12) + .split('') + .map((char, charIndex) => ( +
+ {char} +
+ ))} +
+ + (× {NO_OF_TILES - index}) + + = + + { + String( + (floats[index] * (NO_OF_TILES - index)).toFixed(12) + ).split('.')[0] + } + + . + { + String( + (floats[index] * (NO_OF_TILES - index)).toFixed(12) + ).split('.')[1] + } + + +
+
+ ); + })} +
+
+
+ +
+ {fisherYatesShuffle.map(({ array, chosenIndex }, index) => ( +
+
+ {array.map((byte, idx) => ( +
+ {byte} +
+ ))} +
+
+ ))} +
+
+
+ ); +} + +export default MinesResultBreakdown; diff --git a/apps/frontend/src/features/games/mines/index.tsx b/apps/frontend/src/features/games/mines/index.tsx new file mode 100644 index 0000000..b055679 --- /dev/null +++ b/apps/frontend/src/features/games/mines/index.tsx @@ -0,0 +1,105 @@ +import { NO_OF_TILES } from '@repo/common/game-utils/mines/constants.js'; +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { BadgeDollarSignIcon } from 'lucide-react'; +import { Games } from '@/const/games'; +import { playRound } from '@/api/games/mines'; +import GameSettingsBar from '../common/components/game-settings'; +import BettingControls from './components/BettingControls'; +import useMinesStore from './store/minesStore'; +import { + useIsGameActive, + useIsGameLost, + useIsGameWon, + useLastRound, + useMines, + usePayoutMultiplier, + useSelectedTiles, + useTotalPayout, +} from './store/minesSelectors'; +import ActiveGameTile from './components/ActiveGameTile'; +import InactiveGameTile from './components/InactiveGameTile'; + +export function Mines(): JSX.Element { + const { setGameState, gameState } = useMinesStore(); + const [loadingTiles, setLoadingTiles] = useState>(new Set()); + const { mutate: play } = useMutation({ + mutationKey: ['mines-play-round'], + mutationFn: (selectedTileIndex: number) => playRound(selectedTileIndex), + onSuccess: ({ data }) => { + setGameState(data); + setLoadingTiles(prev => { + const newSet = new Set(prev); + data.state.rounds.forEach(round => { + newSet.delete(round.selectedTileIndex); + }); + return newSet; + }); + }, + }); + + const isGameActive = useIsGameActive(); + + const isGameWon = useIsGameWon(); + const isGameLost = useIsGameLost(); + const mines = useMines(); + const selectedTiles = useSelectedTiles(); + const lastRound = useLastRound(); + const payoutMultiplier = usePayoutMultiplier(); + const payout = useTotalPayout(); + return ( + <> +
+ +
+
+ {Array.from({ length: NO_OF_TILES }, (_, i) => i).map(number => + isGameActive || !gameState ? ( + { + if (isGameActive) { + setLoadingTiles(prev => { + const newSet = new Set(prev); + newSet.add(number); + return newSet; + }); + play(number); + } + }} + /> + ) : ( + + ) + )} +
+ {isGameWon ? ( +
+

+ {payoutMultiplier || '1.00'}x +

+
+

+ {payout || 0} + +

+
+ ) : null} +
+
+ + + ); +} diff --git a/apps/frontend/src/features/games/mines/store/minesSelectors.ts b/apps/frontend/src/features/games/mines/store/minesSelectors.ts new file mode 100644 index 0000000..c9815d0 --- /dev/null +++ b/apps/frontend/src/features/games/mines/store/minesSelectors.ts @@ -0,0 +1,54 @@ +import type { MinesRound } from '@repo/common/game-utils/mines/types.js'; +import useMinesStore from './minesStore'; + +export const useIsGameActive = (): boolean => + useMinesStore(state => state.gameState?.active ?? false); + +export const useIsGameWon = (): boolean => { + const isGameActive = useIsGameActive(); + return useMinesStore(state => + !isGameActive && state.gameState && 'payoutMultiplier' in state.gameState + ? Boolean(state.gameState.payoutMultiplier > 0) + : false + ); +}; + +export const useIsGameLost = (): boolean => { + const isGameActive = useIsGameActive(); + const isGameWon = useIsGameWon(); + return !isGameActive && !isGameWon; +}; + +export const useLastRound = (): MinesRound | null => + useMinesStore(state => state.gameState?.state.rounds.at(-1) ?? null); + +export const useTotalPayout = (): number | null => + useMinesStore(state => + state.gameState && 'payout' in state.gameState + ? state.gameState.payout + : null + ); + +export const usePayoutMultiplier = (): number | null => + useMinesStore(state => + state.gameState && 'payoutMultiplier' in state.gameState + ? state.gameState.payoutMultiplier + : null + ); + +export const useSelectedTiles = (): Set | null => + useMinesStore(state => { + if (!state.gameState) return null; + return new Set( + state.gameState.state.rounds.map(round => round.selectedTileIndex) + ); + }); + +export const useMines = (): Set | null => + useMinesStore(state => { + if (!state.gameState) return null; + return new Set(state.gameState.state.mines); + }); + +export const useMinesCount = (): number | null => + useMinesStore(state => state.gameState?.state.minesCount ?? null); diff --git a/apps/frontend/src/features/games/mines/store/minesStore.ts b/apps/frontend/src/features/games/mines/store/minesStore.ts new file mode 100644 index 0000000..30687cf --- /dev/null +++ b/apps/frontend/src/features/games/mines/store/minesStore.ts @@ -0,0 +1,33 @@ +import type { + MinesGameOverResponse, + MinesPlayRoundResponse, +} from '@repo/common/game-utils/mines/types.js'; +import { create } from 'zustand'; + +interface MinesStore { + betAmount: number; + setBetAmount: (betAmount: number) => void; + minesCount: number; + setMinesCount: (minesCount: number) => void; + gameState: MinesPlayRoundResponse | MinesGameOverResponse | null; + setGameState: ( + gameState: MinesPlayRoundResponse | MinesGameOverResponse | null + ) => void; +} + +const useMinesStore = create(set => ({ + betAmount: 0, + setBetAmount: betAmount => { + set({ betAmount }); + }, + minesCount: 3, + setMinesCount: minesCount => { + set({ minesCount }); + }, + gameState: null, + setGameState: gameState => { + set({ gameState }); + }, +})); + +export default useMinesStore; diff --git a/apps/frontend/src/features/games/plinkoo.tsx b/apps/frontend/src/features/games/plinkoo.tsx new file mode 100644 index 0000000..d5486d0 --- /dev/null +++ b/apps/frontend/src/features/games/plinkoo.tsx @@ -0,0 +1,8 @@ +export function Plinkoo(): JSX.Element { + return ( +
+

Plinkoo Game

+

Plinkoo game coming soon...

+
+ ); +} diff --git a/apps/frontend/src/features/games/roulette/components/BettingControls.tsx b/apps/frontend/src/features/games/roulette/components/BettingControls.tsx new file mode 100644 index 0000000..732f3b6 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/BettingControls.tsx @@ -0,0 +1,48 @@ +import { BetAmountInput } from '../../common/components/BetAmountInput'; +import { BetButton } from '../../common/components/BettingControls'; +import useRouletteStore from '../store/rouletteStore'; +import ChipCarousel from './ChipCarousel'; + +interface BettingControlsProps { + betButtonText?: string; + icon?: React.ReactNode; + isDisabled: boolean; + isPending: boolean; + onBet: () => void; +} + +function BettingControls({ + isDisabled, + isPending, + onBet, + betButtonText, + icon, +}: BettingControlsProps): JSX.Element { + const { betAmount, setBetAmount, multiplyBets } = useRouletteStore(); + return ( +
+ + { + setBetAmount(amount * 100 * multiplier); + multiplyBets(multiplier); + }} + /> + { + if (isDisabled) return; + onBet(); + }} + /> +
+ ); +} + +export default BettingControls; diff --git a/apps/frontend/src/features/games/roulette/components/Chip.tsx b/apps/frontend/src/features/games/roulette/components/Chip.tsx new file mode 100644 index 0000000..13d3060 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/Chip.tsx @@ -0,0 +1,117 @@ +import { useMemo } from 'react'; +import { BadgeDollarSignIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { formatCompactNumber, getYellowToRedColor } from '@/lib/formatters'; +import CommonTooltip from '@/components/ui/common-tooltip'; + +interface ChipProps { + size?: number; + value?: number; + customColor?: string; + disabled?: boolean; + onClick?: () => void; + isSelected?: boolean; + id?: string; +} + +function Chip({ + size = 10, + value = 1, + customColor, + disabled, + onClick, + isSelected, + id, +}: ChipProps): JSX.Element { + // Calculate color based on value + // Scale with logarithmic values to better handle a wide range of chip values + // Example ranges: + // 1 to 10 = bright yellow + // 100 to 1000 = orange + // 10,000+ = bright red + const chipColor = useMemo(() => { + if (customColor) return customColor; + + // Use log scale for better distribution across a wide range of values + const logValue = Math.log10(Math.max(1, value)); + // Using our modified getYellowToRedColor which now transitions from yellow (255,206,0) to deep red (180,0,0) + return getYellowToRedColor(logValue, 0, 15); + }, [value, customColor]); + + // Calculate a darker version of the chip color for the shadow + const shadowColor = useMemo(() => { + // Get RGB values from the chipColor + const rgbMatch = /rgb\((?\d+),\s*(?\d+),\s*(?\d+)\)/.exec( + chipColor + ); + if (rgbMatch?.groups) { + const r = Math.max(0, parseInt(rgbMatch.groups.red, 10) - 80); // Reduce red by 80 + const g = Math.max(0, parseInt(rgbMatch.groups.green, 10) - 60); // Reduce green by 60 + const b = Math.max(0, parseInt(rgbMatch.groups.blue, 10)); // Keep blue as is + return `rgb(${r}, ${g}, ${b})`; + } + return 'rgb(144, 102, 0)'; // Default shadow color + }, [chipColor]); + + // Get formatted value text + const formattedValue = formatCompactNumber(value); + + // Generate box shadow based on disabled state + const boxShadowStyle = isSelected + ? `${shadowColor} 0px 0.125rem 0px 0px, rgba(255, 255, 255) 0px 0.065rem 0px 0.2rem` + : `${shadowColor} 0px 0.125rem 0px 0px`; + + return ( + + {value / 100} + + + +
+ } + > +
{ + if (!disabled) { + onClick?.(); + } + }} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + if (!disabled) { + onClick?.(); + } + } + }} + role="button" + style={{ + backgroundColor: chipColor, + width: `${size * 4}px`, + height: `${size * 4}px`, + boxShadow: boxShadowStyle, + }} + tabIndex={0} + > + + {formattedValue} + +
+ + ); +} + +export default Chip; diff --git a/apps/frontend/src/features/games/roulette/components/ChipCarousel.tsx b/apps/frontend/src/features/games/roulette/components/ChipCarousel.tsx new file mode 100644 index 0000000..7dec209 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/ChipCarousel.tsx @@ -0,0 +1,88 @@ +import { useRef, useEffect } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { Label } from '@/components/ui/label'; +import useRouletteStore from '../store/rouletteStore'; +import ScrollNextButton from './ScrollNextButton'; +import ScrollPrevButton from './ScrollPrevButton'; +import Chip from './Chip'; + +function ChipCarousel(): JSX.Element { + const scrollContainerRef = useRef(null); + const queryClient = useQueryClient(); + const balance = queryClient.getQueryData(['balance']) || 0; + + const { betAmount, selectedChip, setSelectedChip } = useRouletteStore(); + const scrollLeft = (): void => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollBy({ + left: -100, + behavior: 'smooth', + }); + } + }; + + const scrollRight = (): void => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollBy({ + left: 100, + behavior: 'smooth', + }); + } + }; + + useEffect(() => { + if (selectedChip && balance && betAmount + selectedChip > balance * 100) { + if (selectedChip < 1) { + setSelectedChip(null); + } else { + setSelectedChip(selectedChip / 10); + } + } + }, [selectedChip, betAmount, balance, setSelectedChip]); + + return ( +
+ {balance ? ( + + ) : null} +
+ +
+
+ {Array.from({ + length: 10, + }).map((_, index) => { + const chipValue = 10 ** index; + const isDisabled = balance * 100 - betAmount < chipValue; + + return ( + { + setSelectedChip(chipValue); + }} + size={8} + value={chipValue} + /> + ); + })} +
+
+ + +
+
+ ); +} + +export default ChipCarousel; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteResultBreakdown.tsx b/apps/frontend/src/features/games/roulette/components/RouletteResultBreakdown.tsx new file mode 100644 index 0000000..1a5b389 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteResultBreakdown.tsx @@ -0,0 +1,164 @@ +import { Fragment, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { HashLoader } from 'react-spinners'; +import { getGeneratedFloats, byteGenerator } from '@/lib/crypto'; +import { Label } from '@/components/ui/label'; +import { cn } from '@/lib/utils'; +// Simple function to +// generate a stable unique ID without using array indices +const generateUniqueId = ( + prefix: string, + ...parts: (string | number)[] +): string => { + return `${prefix}-${parts.join('-')}`; +}; + +interface RouletteResultBreakdownProps { + nonce?: string; + serverSeed?: string; + clientSeed?: string; +} + +function RouletteResultBreakdown({ + nonce, + serverSeed, + clientSeed, +}: RouletteResultBreakdownProps): JSX.Element { + const { data: hmacArray = [] } = useQuery({ + queryKey: ['hmacBuffer', serverSeed, clientSeed, nonce], + queryFn: async () => { + const bytes = await byteGenerator( + serverSeed ?? '', + `${clientSeed}:${nonce}`, + 1 + ); + return bytes; + }, + }); + + const { data: outcome } = useQuery({ + queryKey: ['result', serverSeed, clientSeed, nonce], + queryFn: async () => { + const result = await getGeneratedFloats({ + count: 1, + seed: serverSeed ?? '', + message: `${clientSeed}:${nonce}`, + }); + return result[0]; + }, + }); + + const selectedBytes = hmacArray.slice(0, 4); + + // Create unique identifiers for each byte in the hmacArray + const hmacByteIds = useMemo(() => { + return hmacArray.map((byte, idx) => ({ + byte, + id: generateUniqueId('byte', byte, idx, Date.now()), + })); + }, [hmacArray]); + + // Create unique identifiers for selected bytes + const selectedByteIds = useMemo(() => { + return selectedBytes.map((byte, idx) => ({ + byte, + id: generateUniqueId('selected-byte', byte, idx, Date.now()), + })); + }, [selectedBytes]); + + if (!serverSeed || !clientSeed || !outcome) { + return ; + } + + const finalOutcome = Math.floor(outcome * 37); + + return ( +
+
+ +

{finalOutcome}

+
+
+ +

+ {`HMAC_SHA256(${serverSeed}, ${clientSeed}:${nonce}:0)`} +

+
+ {hmacByteIds.map(({ byte, id }, index) => ( +
+ {byte.toString(16).padStart(2, '0')} + {byte} +
+ ))} +
+
+
+ +
+
+
{`(${selectedBytes.join(', ')}) -> [0, ..., 36] = ${finalOutcome}`}
+ {selectedByteIds.map(({ byte, id }, index) => ( + + + {index > 0 ? '+' : ''} + + + {(byte / 256 ** (index + 1)) + .toFixed(12) + .split('') + .map((char, charIndex) => ( +
+ {char} +
+ ))} +
+ + {`(${Array((3 - (byte.toString().length % 3)) % 3) + .fill('0') + .join('')}${byte} / (256 ^ ${index + 1}))`} + +
+ ))} + = + + {outcome + .toFixed(12) + .split('') + .map((char, charIndex) => ( +
+ {char} +
+ ))} +
+ + (× 37) + + = + + {String((outcome * 37).toFixed(12)).split('.')[0]} + + .{String((outcome * 37).toFixed(12)).split('.')[1]} + + +
+
+
+
+ ); +} + +export default RouletteResultBreakdown; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteTable/BottomBets.tsx b/apps/frontend/src/features/games/roulette/components/RouletteTable/BottomBets.tsx new file mode 100644 index 0000000..6cc28ad --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteTable/BottomBets.tsx @@ -0,0 +1,47 @@ +import { RouletteBetTypes } from '@repo/common/game-utils/roulette/validations.js'; +import BottomNumberBets from './BottomNumberBets'; +import BottomColorBets from './BottomColorBets'; + +const bottomBets = [ + { + action: RouletteBetTypes.LOW, + label: '1 to 18', + }, + { + action: RouletteBetTypes.EVEN, + label: 'Even', + }, + { + action: RouletteBetTypes.RED, + label: null, + }, + { + action: RouletteBetTypes.BLACK, + label: null, + }, + { + action: RouletteBetTypes.ODD, + label: 'Odd', + }, + { + action: RouletteBetTypes.HIGH, + label: '19 to 36', + }, +]; + +function BottomBets(): JSX.Element { + return ( + <> + {bottomBets.map(({ action, label }) => { + if (typeof label === 'string') { + return ( + + ); + } + return ; + })} + + ); +} + +export default BottomBets; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteTable/BottomColorBets.tsx b/apps/frontend/src/features/games/roulette/components/RouletteTable/BottomColorBets.tsx new file mode 100644 index 0000000..c2a5385 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteTable/BottomColorBets.tsx @@ -0,0 +1,105 @@ +import { sum } from 'lodash'; +import { + redNumbers, + blackNumbers, +} from '@repo/common/game-utils/roulette/constants.js'; +import { useMemo } from 'react'; +import { motion } from 'motion/react'; +import { RouletteBetTypes } from '@repo/common/game-utils/roulette/validations.js'; +import { cn } from '@/lib/utils'; +import { useRouletteBoardHoverStore } from '../../store/rouletteBoardHoverStore'; +import useRouletteStore from '../../store/rouletteStore'; +import Chip from '../Chip'; +import { + useWinningNumber, + useBetKey, +} from '../../store/rouletteStoreSelectors'; +import { useRouletteContext } from '../../context/RouletteContext'; + +function BottomColorBets({ + action, +}: { + action: RouletteBetTypes; +}): JSX.Element { + const { setHoverId } = useRouletteBoardHoverStore(); + + const betId = action; + const { bets, addBet, isRouletteWheelStopped } = useRouletteStore(); + + const isBet = bets[betId] && bets[betId].length > 0; + const betAmount = sum(bets[betId]); + + const { isPreview } = useRouletteContext(); + + const winningNumber = useWinningNumber(); + const betKey = useBetKey(); + const isWinning = useMemo(() => { + if (!winningNumber || !isRouletteWheelStopped) return false; + switch (action) { + case RouletteBetTypes.RED: + return redNumbers.includes(winningNumber.toString()); + case RouletteBetTypes.BLACK: + return blackNumbers.includes(winningNumber.toString()); + default: + return false; + } + }, [action, isRouletteWheelStopped, winningNumber]); + + return ( + { + e.stopPropagation(); + if (!isPreview) { + addBet(betId); + } + }} + onKeyDown={event => { + return event; + }} + onMouseEnter={() => { + setHoverId(action); + }} + onMouseLeave={() => { + setHoverId(null); + }} + role="button" + tabIndex={0} + transition={ + isWinning + ? { + duration: 1, + repeat: Infinity, + } + : { + duration: 0, + repeat: 0, + } + } + > + {isBet ? ( +
+ +
+ ) : null} +
+ ); +} + +export default BottomColorBets; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteTable/BottomNumberBets.tsx b/apps/frontend/src/features/games/roulette/components/RouletteTable/BottomNumberBets.tsx new file mode 100644 index 0000000..d5efb77 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteTable/BottomNumberBets.tsx @@ -0,0 +1,99 @@ +import { useMemo } from 'react'; +import sum from 'lodash/sum'; +import { motion } from 'motion/react'; +import { RouletteBetTypes } from '@repo/common/game-utils/roulette/validations.js'; +import { cn } from '@/lib/utils'; +import { useRouletteBoardHoverStore } from '../../store/rouletteBoardHoverStore'; +import useRouletteStore from '../../store/rouletteStore'; +import Chip from '../Chip'; +import { + useWinningNumber, + useBetKey, +} from '../../store/rouletteStoreSelectors'; +import { useRouletteContext } from '../../context/RouletteContext'; + +function BottomNumberBets({ + action, + label, +}: { + action: string; + label: string; +}): JSX.Element { + const { setHoverId } = useRouletteBoardHoverStore(); + const betId = action; + const { bets, addBet, isRouletteWheelStopped } = useRouletteStore(); + const isBet = bets[betId] && bets[betId].length > 0; + const betAmount = sum(bets[betId]); + const winningNumber = useWinningNumber(); + const betKey = useBetKey(); + const { isPreview } = useRouletteContext(); + const isWinning = useMemo(() => { + if (!winningNumber || !isRouletteWheelStopped || winningNumber === '0') + return false; + switch (action as RouletteBetTypes) { + case RouletteBetTypes.LOW: + return Number(winningNumber) >= 1 && Number(winningNumber) <= 18; + case RouletteBetTypes.HIGH: + return Number(winningNumber) >= 19 && Number(winningNumber) <= 36; + case RouletteBetTypes.EVEN: + return Number(winningNumber) % 2 === 0; + case RouletteBetTypes.ODD: + return Number(winningNumber) % 2 !== 0; + default: + return false; + } + }, [action, isRouletteWheelStopped, winningNumber]); + + return ( + { + e.stopPropagation(); + if (!isPreview) { + addBet(betId); + } + }} + onKeyDown={event => { + return event; + }} + onMouseEnter={() => { + setHoverId(action); + }} + onMouseLeave={() => { + setHoverId(null); + }} + role="button" + tabIndex={0} + transition={ + isWinning + ? { + duration: 1, + repeat: Infinity, + } + : { + duration: 0, + repeat: 0, + } + } + > + {label} + {isBet ? ( +
+ +
+ ) : null} +
+ ); +} + +export default BottomNumberBets; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteTable/ColumnBet.tsx b/apps/frontend/src/features/games/roulette/components/RouletteTable/ColumnBet.tsx new file mode 100644 index 0000000..0df89dd --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteTable/ColumnBet.tsx @@ -0,0 +1,89 @@ +import { sum } from 'lodash'; +import { useMemo } from 'react'; +import { motion } from 'motion/react'; +import { RouletteBetTypes } from '@repo/common/game-utils/roulette/validations.js'; +import { cn } from '@/lib/utils'; +import { useRouletteBoardHoverStore } from '../../store/rouletteBoardHoverStore'; +import useRouletteStore from '../../store/rouletteStore'; +import Chip from '../Chip'; +import { + useWinningNumber, + useBetKey, +} from '../../store/rouletteStoreSelectors'; +import { useRouletteContext } from '../../context/RouletteContext'; + +function ColumnBet({ column }: { column: number }): JSX.Element { + const { setHoverId } = useRouletteBoardHoverStore(); + + const betId = `${RouletteBetTypes.COLUMN}-${column}`; + const { bets, addBet, isRouletteWheelStopped } = useRouletteStore(); + + const { isPreview } = useRouletteContext(); + + const isBet = bets[betId] && bets[betId].length > 0; + const betAmount = sum(bets[betId]); + + const winningNumber = useWinningNumber(); + const betKey = useBetKey(); + const isWinning = useMemo(() => { + if (!isRouletteWheelStopped) return false; + return ( + winningNumber && + winningNumber !== '0' && + Number(winningNumber) % 3 === column % 3 + ); + }, [column, isRouletteWheelStopped, winningNumber]); + + return ( + { + e.stopPropagation(); + if (!isPreview) { + addBet(betId); + } + }} + onKeyDown={event => { + return event; + }} + onMouseEnter={() => { + setHoverId(betId); + }} + onMouseLeave={() => { + setHoverId(null); + }} + role="button" + tabIndex={0} + transition={ + isWinning + ? { + duration: 1, + repeat: Infinity, + } + : { + duration: 0, + repeat: 0, + } + } + > + 2:1 + {isBet ? ( +
+ +
+ ) : null} +
+ ); +} + +export default ColumnBet; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteTable/DozenBet.tsx b/apps/frontend/src/features/games/roulette/components/RouletteTable/DozenBet.tsx new file mode 100644 index 0000000..ae3b4a0 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteTable/DozenBet.tsx @@ -0,0 +1,89 @@ +import { sum } from 'lodash'; +import { motion } from 'motion/react'; +import { useMemo } from 'react'; +import { RouletteBetTypes } from '@repo/common/game-utils/roulette/validations.js'; +import { cn } from '@/lib/utils'; +import { useRouletteBoardHoverStore } from '../../store/rouletteBoardHoverStore'; +import useRouletteStore from '../../store/rouletteStore'; +import Chip from '../Chip'; +import { + useWinningNumber, + useBetKey, +} from '../../store/rouletteStoreSelectors'; +import { useRouletteContext } from '../../context/RouletteContext'; + +function DozenBet({ dozen }: { dozen: number }): JSX.Element { + const { setHoverId } = useRouletteBoardHoverStore(); + + const { isPreview } = useRouletteContext(); + + const betId = `${RouletteBetTypes.DOZEN}-${dozen}`; + const { bets, addBet, isRouletteWheelStopped } = useRouletteStore(); + + const isBet = bets[betId] && bets[betId].length > 0; + const betAmount = sum(bets[betId]); + + const winningNumber = useWinningNumber(); + const betKey = useBetKey(); + const isWinning = useMemo(() => { + if (!isRouletteWheelStopped) return false; + return ( + winningNumber && + Number(winningNumber) > (dozen - 1) * 12 && + Number(winningNumber) <= dozen * 12 + ); + }, [dozen, isRouletteWheelStopped, winningNumber]); + + return ( + { + e.stopPropagation(); + if (!isPreview) { + addBet(betId); + } + }} + onKeyDown={event => { + return event; + }} + onMouseEnter={() => { + setHoverId(betId); + }} + onMouseLeave={() => { + setHoverId(null); + }} + role="button" + tabIndex={0} + transition={ + isWinning + ? { + duration: 1, + repeat: Infinity, + } + : { + duration: 0, + repeat: 0, + } + } + > + {12 * (dozen - 1) + 1} to {12 * (dozen - 1) + 12} + {isBet ? ( +
+ +
+ ) : null} +
+ ); +} + +export default DozenBet; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteTable/DroppableArea.tsx b/apps/frontend/src/features/games/roulette/components/RouletteTable/DroppableArea.tsx new file mode 100644 index 0000000..2803bd3 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteTable/DroppableArea.tsx @@ -0,0 +1,129 @@ +import { useEffect, useState } from 'react'; +import { sum } from 'lodash'; +import { getBetTypeSelectionId } from '../../utils/helpers'; +import useRouletteStore from '../../store/rouletteStore'; +import Chip from '../Chip'; +import { useRouletteBoardHoverStore } from '../../store/rouletteBoardHoverStore'; +import { useRouletteContext } from '../../context/RouletteContext'; + +interface PositionValues { + top: string | number; + left: string | number; + transform?: string; +} + +const POSITIONS: Record = { + TL: { top: '0%', left: '0%', transform: 'translate(-50%, -50%)' }, + TC: { top: '0%', left: '50%', transform: 'translate(-50%, -50%)' }, + TR: { + top: '0%', + left: 'calc(100% + 2px)', + transform: 'translate(-50%, -50%)', + }, + CR: { + top: '50%', + left: 'calc(100% + 2px)', + transform: 'translate(-50%, -50%)', + }, + BC: { top: '100%', left: '50%', transform: 'translate(-50%, -50%)' }, + BR: { + top: 'calc(100% + 2px)', + left: 'calc(100% + 2px)', + transform: 'translate(-50%, -50%)', + }, + + // Custom Positions for 0-1, 0-2, 0-3 in Roulette with enlarged hit areas + '0-1': { + top: `83.33%`, + left: 'calc(100% + 2px)', + transform: 'translate(-50%, -50%)', + }, + '0-3': { + top: `16.66%`, + left: 'calc(100% + 2px)', + transform: 'translate(-50%, -50%)', + }, +}; + +function DroppableArea({ + position, + reference, + betTypeData, + width, + height, +}: { + position: keyof typeof POSITIONS; + reference: React.RefObject; + betTypeData: { + betType: string; + selection: number | number[] | null; + }; + width?: string; + height?: string; +}): JSX.Element { + const betId = `${betTypeData.betType}-${getBetTypeSelectionId( + betTypeData.selection || null + )}`; + + const { setHoverId } = useRouletteBoardHoverStore(); + + const { isPreview } = useRouletteContext(); + + const { bets, addBet } = useRouletteStore(); + + const [style, setStyle] = useState({}); + + useEffect(() => { + if (reference.current) { + const rect = reference.current.getBoundingClientRect(); + + const { top, left, transform } = POSITIONS[position]; + + setStyle({ + position: 'absolute', + top: typeof top === 'number' ? rect.top + top : top, + left: typeof left === 'number' ? rect.left + left : left, + transform: transform || undefined, + width: width || '20px', + height: height || '20px', + zIndex: 10, + }); + } + }, [position, reference, width, height]); + + const isBet = bets[betId] && bets[betId].length > 0; + const betAmount = sum(bets[betId]); + + return ( +
{ + e.stopPropagation(); + if (isPreview) return; + addBet(betId); + }} + onKeyDown={event => { + return event; + }} + onMouseEnter={() => { + setHoverId(betId); + }} + onMouseLeave={() => { + setHoverId(null); + }} + role="button" + style={style} + tabIndex={0} + > + {/* Use a transparent div with the full size for the hit area */} +
+ {isBet ? ( +
+ +
+ ) : null} +
+
+ ); +} + +export default DroppableArea; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteTable/NumberBet.tsx b/apps/frontend/src/features/games/roulette/components/RouletteTable/NumberBet.tsx new file mode 100644 index 0000000..26ce4cf --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteTable/NumberBet.tsx @@ -0,0 +1,178 @@ +import { redNumbers } from '@repo/common/game-utils/roulette/constants.js'; +import { RouletteBetTypes } from '@repo/common/game-utils/roulette/index.js'; +import { useRef } from 'react'; +import { sum } from 'lodash'; +import { motion } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useRouletteBoardHoverStore } from '../../store/rouletteBoardHoverStore'; +import { getIsNumberHover } from '../../utils/hover'; +import { + shouldRenderBottom, + shouldRenderCornerBet, + shouldRenderRight, + shouldRenderSixLineBet, + shouldRenderTop, +} from '../../utils/shouldRender'; +import useRouletteStore from '../../store/rouletteStore'; +import Chip from '../Chip'; +import { + useWinningNumber, + useBetKey, +} from '../../store/rouletteStoreSelectors'; +import { useRouletteContext } from '../../context/RouletteContext'; +import DroppableArea from './DroppableArea'; + +function NumberBet({ number }: { number: number }): JSX.Element { + const { hoverId } = useRouletteBoardHoverStore(); + const winningNumber = useWinningNumber(); + const betKey = useBetKey(); + const { isPreview } = useRouletteContext(); + const referenceDiv = useRef(null); + + const isRedNumber = redNumbers.includes(number.toString()); + const isNumberHover = !isPreview && getIsNumberHover({ number, hoverId }); + + const betId = `${RouletteBetTypes.STRAIGHT}-${number}`; + + const { bets, addBet, isRouletteWheelStopped } = useRouletteStore(); + + const isBet = bets[betId] && bets[betId].length > 0; + const betAmount = sum(bets[betId]); + + return ( + { + e.stopPropagation(); + if (!isPreview) { + addBet(betId); + } + }} + onKeyDown={event => { + return event; + }} + ref={el => { + referenceDiv.current = el; + }} + role="button" + tabIndex={0} + transition={ + isRouletteWheelStopped && Number(winningNumber) === number + ? { + duration: 1, + repeat: Infinity, + } + : { + duration: 0, + repeat: 0, + } + } + > + {number} + {isBet ? ( +
+ +
+ ) : null} + + {shouldRenderCornerBet(number) && ( + + )} + {number === 1 && ( + + )} + {number === 2 && ( + + )} + {shouldRenderTop(number) && ( + + )} + {shouldRenderRight(number) && ( + + )} + {shouldRenderBottom(number) && ( + + )} + {shouldRenderSixLineBet(number) && ( + + )} +
+ ); +} + +export default NumberBet; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteTable/ZeroBet.tsx b/apps/frontend/src/features/games/roulette/components/RouletteTable/ZeroBet.tsx new file mode 100644 index 0000000..dd54c28 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteTable/ZeroBet.tsx @@ -0,0 +1,118 @@ +import { useRef } from 'react'; +import { sum } from 'lodash'; +import { motion } from 'motion/react'; +import { RouletteBetTypes } from '@repo/common/game-utils/roulette/validations.js'; +import { cn } from '@/lib/utils'; +import { useRouletteBoardHoverStore } from '../../store/rouletteBoardHoverStore'; +import { getIsNumberHover } from '../../utils/hover'; +import useRouletteStore from '../../store/rouletteStore'; +import Chip from '../Chip'; +import { + useWinningNumber, + useBetKey, +} from '../../store/rouletteStoreSelectors'; +import { useRouletteContext } from '../../context/RouletteContext'; +import DroppableArea from './DroppableArea'; + +function ZeroBet(): JSX.Element { + const { hoverId } = useRouletteBoardHoverStore(); + const referenceDiv = useRef(null); + + const { isPreview } = useRouletteContext(); + + const isNumberHover = !isPreview && getIsNumberHover({ number: 0, hoverId }); + const winningNumber = useWinningNumber(); + + const betId = `${RouletteBetTypes.STRAIGHT}-0`; + const { bets, addBet, isRouletteWheelStopped } = useRouletteStore(); + + const isBet = bets[betId] && bets[betId].length > 0; + const betAmount = sum(bets[betId]); + + const isWinning = isRouletteWheelStopped && Number(winningNumber) === 0; + const betKey = useBetKey(); + return ( + { + e.stopPropagation(); + if (!isPreview) { + addBet(betId); + } + }} + onKeyDown={event => { + return event; + }} + ref={el => { + referenceDiv.current = el; + }} + role="button" + tabIndex={0} + transition={ + isWinning + ? { + duration: 1, + repeat: Infinity, + } + : { + duration: 0, + repeat: 0, + } + } + > + 0 + {isBet ? ( +
+ +
+ ) : null} + + + + +
+ ); +} + +export default ZeroBet; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteTable/index.tsx b/apps/frontend/src/features/games/roulette/components/RouletteTable/index.tsx new file mode 100644 index 0000000..556fa0e --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteTable/index.tsx @@ -0,0 +1,47 @@ +import ZeroBet from './ZeroBet'; +import NumberBet from './NumberBet'; +import ColumnBet from './ColumnBet'; +import DozenBet from './DozenBet'; +import BottomBets from './BottomBets'; + +function RouletteTable(): JSX.Element { + return ( +
+
+ + {Array.from({ length: 12 }, (_, index) => index).map(colNum => ( +
+ {Array.from({ length: 3 }, (_, index) => index + 1).map(rowNum => { + return ( + + ); + })} +
+ ))} +
+ + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ ); +} + +export default RouletteTable; diff --git a/apps/frontend/src/features/games/roulette/components/RouletteWheel.tsx b/apps/frontend/src/features/games/roulette/components/RouletteWheel.tsx new file mode 100644 index 0000000..de90bad --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/RouletteWheel.tsx @@ -0,0 +1,183 @@ +import { motion, useAnimation } from 'motion/react'; +import { useEffect } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { cn } from '@/lib/utils'; +import useRouletteStore from '../store/rouletteStore'; + +const normalizeAngle = (angle: number): number => ((angle % 360) + 360) % 360; + +const rouletteWheelNumbers = [ + '0', + '32', + '15', + '19', + '4', + '21', + '2', + '25', + '17', + '34', + '6', + '27', + '13', + '36', + '11', + '30', + '8', + '23', + '10', + '5', + '24', + '16', + '33', + '1', + '20', + '14', + '31', + '9', + '22', + '18', + '29', + '7', + '28', + '12', + '35', + '3', + '26', +]; + +function RouletteWheel({ + isSpinning, + winningNumber, + isPreview = false, +}: { + isSpinning: boolean; + winningNumber: string | null; + isPreview?: boolean; +}): JSX.Element { + const selectedNumber = winningNumber || '0'; + const radius = 202; + const angleStep = 360 / 37; + const textRadius = radius - 25; // Move text inward + const textOffset = 15; // Small shift left/right inside the segment + const selectedIndex = rouletteWheelNumbers.indexOf(selectedNumber); + const rotationOffset = + selectedIndex !== -1 ? -(selectedIndex * angleStep) + 90 : 0; + + const rotationAngle = normalizeAngle(rotationOffset); + + const controls = useAnimation(); + const queryClient = useQueryClient(); + const { setIsRouletteWheelStopped } = useRouletteStore(); + useEffect(() => { + if (isSpinning && !isPreview) + void controls.start({ + rotate: [rotationAngle, rotationAngle + 360], + transition: { repeat: Infinity, duration: 1, ease: 'linear' }, + }); + }, [controls, isSpinning, rotationAngle, isPreview]); + + useEffect(() => { + if (winningNumber && !isPreview) { + controls.stop(); + void controls + .start({ + rotate: [rotationAngle, rotationAngle + 360], + transition: { duration: 1, ease: 'easeOut' }, + }) + .then(() => { + setIsRouletteWheelStopped(true); + void queryClient.invalidateQueries({ queryKey: ['balance'] }); + }); + } + }, [ + controls, + queryClient, + rotationAngle, + setIsRouletteWheelStopped, + winningNumber, + isPreview, + ]); + + return ( +
+ {/* The fixed arrow that doesn't rotate with the wheel */} +
+ Roulette Wheel Arrow +
+ + + {/* Inner Circle (Clipping Center) */} +
+ +
+ + {/* Segments */} + {Array.from({ length: 37 }, (_, i) => { + const rotation = i * angleStep; + + return ( +
+ ); + })} + + {/* Numbers */} + {rouletteWheelNumbers.map((number, i) => { + const angle = (i - 0.5) * angleStep; // Offset to center of segment + const radian = (angle * Math.PI) / 180; + const x = textRadius * Math.cos(radian); + const y = textRadius * Math.sin(radian); + + // Shift perpendicular to the spoke direction + const shiftX = textOffset * Math.cos((angle + 90) * (Math.PI / 180)); + const shiftY = textOffset * Math.sin((angle + 90) * (Math.PI / 180)); + + return ( +
+ {number} +
+ ); + })} + +
+ ); +} + +export default RouletteWheel; diff --git a/apps/frontend/src/features/games/roulette/components/ScrollNextButton.tsx b/apps/frontend/src/features/games/roulette/components/ScrollNextButton.tsx new file mode 100644 index 0000000..74a5e0f --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/ScrollNextButton.tsx @@ -0,0 +1,19 @@ +import { ChevronRightIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface ScrollNextButtonProps { + onClick?: () => void; +} + +function ScrollNextButton({ onClick }: ScrollNextButtonProps): JSX.Element { + return ( + + ); +} + +export default ScrollNextButton; diff --git a/apps/frontend/src/features/games/roulette/components/ScrollPrevButton.tsx b/apps/frontend/src/features/games/roulette/components/ScrollPrevButton.tsx new file mode 100644 index 0000000..94abe51 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/components/ScrollPrevButton.tsx @@ -0,0 +1,19 @@ +import { ChevronLeftIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface ScrollPrevButtonProps { + onClick?: () => void; +} + +function ScrollPrevButton({ onClick }: ScrollPrevButtonProps): JSX.Element { + return ( + + ); +} + +export default ScrollPrevButton; diff --git a/apps/frontend/src/features/games/roulette/context/RouletteContext.tsx b/apps/frontend/src/features/games/roulette/context/RouletteContext.tsx new file mode 100644 index 0000000..204a031 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/context/RouletteContext.tsx @@ -0,0 +1,32 @@ +import type { ReactNode } from 'react'; +import { createContext, useContext } from 'react'; + +interface RouletteContextType { + isPreview: boolean; +} + +const RouletteContext = createContext( + undefined +); + +export function RouletteProvider({ + isPreview, + children, +}: { + children: ReactNode; + isPreview: boolean; +}): JSX.Element { + return ( + + {children} + + ); +} + +export function useRouletteContext(): RouletteContextType { + const context = useContext(RouletteContext); + if (context === undefined) { + return { isPreview: false }; + } + return context; +} diff --git a/apps/frontend/src/features/games/roulette/index.tsx b/apps/frontend/src/features/games/roulette/index.tsx new file mode 100644 index 0000000..a55a8c5 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/index.tsx @@ -0,0 +1,140 @@ +import { sum } from 'lodash'; +import type { RouletteBet } from '@repo/common/game-utils/roulette/validations.js'; +import { validateBets } from '@repo/common/game-utils/roulette/validations.js'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { BadgeDollarSignIcon, RefreshCcwIcon, Undo2Icon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { placeBet } from '@/api/games/roulette'; +import { getBalance } from '@/api/balance'; +import { Games } from '@/const/games'; +import GameSettingsBar from '../common/components/game-settings'; +import BettingControls from './components/BettingControls'; +import RouletteTable from './components/RouletteTable'; +import RouletteWheel from './components/RouletteWheel'; +import useRouletteStore from './store/rouletteStore'; +import { parseBetId } from './utils/helpers'; +import { RouletteProvider } from './context/RouletteContext'; +import { useWinningNumber } from './store/rouletteStoreSelectors'; + +export function Roulette({ + isPreview = false, +}: { + isPreview?: boolean; +}): JSX.Element { + const { + undoBet, + clearBets, + bets, + betHistory, + betAmount, + setLatestResult, + latestResult, + isRouletteWheelStopped, + setIsRouletteWheelStopped, + } = useRouletteStore(); + const { data: balance = 0 } = useQuery({ + queryKey: ['balance'], + queryFn: getBalance, + refetchInterval: 120000, + // Refetch every 2 minutes + }); + + const { mutate, isPending: isSpinning } = useMutation({ + mutationFn: (rouletteBets: RouletteBet[]) => placeBet(rouletteBets), + onSuccess: ({ data }) => { + setLatestResult(data); + return data; + }, + onError: error => { + return error; + }, + }); + + const winningNumber = useWinningNumber(); + + const onBet = (): void => { + if (latestResult) { + setLatestResult(null); + return; + } + const rouletteBet = Object.entries(bets).map(([betId, amounts]) => { + const { betType, selection } = parseBetId(betId); + const totalBetAmount = sum(amounts) / 100; + return { + betType, + ...(selection === null ? {} : { selection }), + amount: totalBetAmount, + } as RouletteBet; + }); + + const rouletteBets = validateBets(rouletteBet); + setIsRouletteWheelStopped(false); + mutate(rouletteBets); + }; + + return ( + <> + +
+ : null} + isDisabled={ + latestResult + ? false + : betHistory.length === 0 || + isSpinning || + !isRouletteWheelStopped || + balance * 100 < betAmount + } + isPending={isSpinning ? true : !isRouletteWheelStopped} + onBet={onBet} + /> +
+
+ +
+
+ {isRouletteWheelStopped ? winningNumber : null} +
+
+
+ {latestResult?.payout && isRouletteWheelStopped ? ( +
+ {latestResult.payoutMultiplier.toFixed(2)}x + | + {latestResult.payout} + +
+ ) : null} +
+ +
+
+ + +
+
+
+
+ + + ); +} diff --git a/apps/frontend/src/features/games/roulette/store/rouletteBoardHoverStore.ts b/apps/frontend/src/features/games/roulette/store/rouletteBoardHoverStore.ts new file mode 100644 index 0000000..aec123f --- /dev/null +++ b/apps/frontend/src/features/games/roulette/store/rouletteBoardHoverStore.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand'; + +interface RouletteBoardHoverStore { + hoverId: string | null; + setHoverId: (hoverId: string | null) => void; +} + +export const useRouletteBoardHoverStore = create( + set => ({ + hoverId: null, + setHoverId: hoverId => { + set({ hoverId }); + }, + }) +); diff --git a/apps/frontend/src/features/games/roulette/store/rouletteStore.ts b/apps/frontend/src/features/games/roulette/store/rouletteStore.ts new file mode 100644 index 0000000..7e80be2 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/store/rouletteStore.ts @@ -0,0 +1,122 @@ +import type { RoulettePlaceBetResponse } from '@repo/common/game-utils/roulette/types.js'; +import { create } from 'zustand'; + +interface RouletteStoreActions { + setSelectedChip: (chip: number | null) => void; + setBetAmount: (betAmount: number) => void; + addBetHistory: (betHistory: string) => void; + clearBets: () => void; + undoBet: () => void; + addBet: (betId: string) => void; + updateBetAmount: (betAmount: number) => void; + multiplyBets: (multiplier: number) => void; + setLatestResult: (result: RoulettePlaceBetResponse | null) => void; + setIsRouletteWheelStopped: (isStopped: boolean) => void; +} + +interface RouletteStoreState { + selectedChip: number | null; + betAmount: number; + betHistory: string[]; + bets: Record; + latestResult: RoulettePlaceBetResponse | null; + isRouletteWheelStopped: boolean; +} + +const initialState: RouletteStoreState = { + betAmount: 0, + betHistory: [], + bets: {}, + selectedChip: 1, + latestResult: null, + isRouletteWheelStopped: true, +}; + +const useRouletteStore = create( + set => ({ + ...initialState, + setBetAmount: betAmount => { + set({ betAmount }); + }, + updateBetAmount: betAmount => { + set(state => ({ betAmount: state.betAmount + betAmount })); + }, + setSelectedChip: chip => { + if (chip && chip < 1) { + set({ selectedChip: null }); + } + set({ selectedChip: chip }); + }, + addBetHistory: betHistory => { + set(state => ({ betHistory: [...state.betHistory, betHistory] })); + }, + clearBets: () => { + set(initialState); + }, + multiplyBets: (multiplier: number) => { + set(state => ({ + bets: Object.fromEntries( + Object.entries(state.bets).map(([key, betAmountArray]) => [ + key, + betAmountArray?.map(betAmount => betAmount * multiplier), + ]) + ), + })); + }, + undoBet: () => { + set(state => { + if (state.betHistory.length === 0) return state; + const lastBetId = state.betHistory[state.betHistory.length - 1]; + + const lastChipOnBoardBetAmountArray = state.bets[lastBetId]; + + const lastChipOnBoardBetAmount = + lastChipOnBoardBetAmountArray?.[ + lastChipOnBoardBetAmountArray.length - 1 + ] || 0; + + return { + betHistory: state.betHistory.slice(0, -1), + bets: { + ...state.bets, + [lastBetId]: state.bets[lastBetId]?.slice(0, -1) || [], + }, + betAmount: state.betAmount - lastChipOnBoardBetAmount, + }; + }); + }, + addBet: betId => { + set(state => { + if (!state.selectedChip) { + return state; + } + + if (state.selectedChip < 1) { + return { selectedChip: null }; + } + + const updatedBets = { ...state.bets }; + + if (!updatedBets[betId]) { + updatedBets[betId] = []; + } + + updatedBets[betId] = [...updatedBets[betId], state.selectedChip]; + + return { + bets: updatedBets, + betAmount: state.betAmount + state.selectedChip, + betHistory: [...state.betHistory, betId], + }; + }); + }, + setLatestResult: result => { + set({ latestResult: result }); + }, + setIsRouletteWheelStopped: isStopped => { + set({ isRouletteWheelStopped: isStopped }); + }, + }) +); + +export default useRouletteStore; diff --git a/apps/frontend/src/features/games/roulette/store/rouletteStoreSelectors.ts b/apps/frontend/src/features/games/roulette/store/rouletteStoreSelectors.ts new file mode 100644 index 0000000..bb33953 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/store/rouletteStoreSelectors.ts @@ -0,0 +1,10 @@ +import useRouletteStore from './rouletteStore'; + +export const useWinningNumber = (): string | null => + useRouletteStore(state => { + const winningNumber = state.latestResult?.state.winningNumber; + return winningNumber || null; + }); + +export const useBetKey = (): string | null => + useRouletteStore(state => state.latestResult?.id ?? null); diff --git a/apps/frontend/src/features/games/roulette/utils/helpers.ts b/apps/frontend/src/features/games/roulette/utils/helpers.ts new file mode 100644 index 0000000..fa47e33 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/utils/helpers.ts @@ -0,0 +1,41 @@ +// This function converts a bet selection (number, array, or null) to a string ID format +const getBetTypeSelectionId = ( + betSelection: number | number[] | null +): string => { + if (typeof betSelection === 'number') { + return betSelection.toString(); + } + if (Array.isArray(betSelection)) { + return betSelection.join('-'); + } + return 'null'; +}; + +// This function parses a bet ID back into its component parts (betType and selection) +const parseBetId = ( + betId: string +): { + betType: string; + selection: number | number[] | null; +} => { + const [betType, ...selectionParts] = betId.split('-'); + + if (selectionParts.length === 0 || selectionParts[0] === 'null') { + return { betType, selection: null }; + } + + if (selectionParts.length === 1) { + return { + betType, + selection: parseInt(selectionParts[0], 10), + }; + } + + // If we have multiple parts, it's an array of numbers + return { + betType, + selection: selectionParts.map(part => parseInt(part, 10)), + }; +}; + +export { getBetTypeSelectionId, parseBetId }; diff --git a/apps/frontend/src/features/games/roulette/utils/hover.ts b/apps/frontend/src/features/games/roulette/utils/hover.ts new file mode 100644 index 0000000..3d15a12 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/utils/hover.ts @@ -0,0 +1,83 @@ +import { + blackNumbers, + redNumbers, +} from '@repo/common/game-utils/roulette/constants.js'; +import { RouletteBetTypes } from '@repo/common/game-utils/roulette/index.js'; + +const getIsColumnHover = ({ + number, + hoverId, +}: { + number: number; + hoverId: string; +}): boolean => { + const column = parseInt(hoverId.split('-')[1]); + return number !== 0 && number % 3 === column % 3; +}; + +const getIsDozenHover = ({ + number, + hoverId, +}: { + number: number; + hoverId: string; +}): boolean => { + const dozen = parseInt(hoverId.split('-')[1]); + return number > (dozen - 1) * 12 && number <= dozen * 12; +}; + +const checkIsChipHover = ({ + number, + hoverId, +}: { + number: number; + hoverId: string; +}): boolean => { + const [betType, ...selection] = hoverId.split('-'); + + switch (betType as RouletteBetTypes) { + case RouletteBetTypes.STRAIGHT: + case RouletteBetTypes.SPLIT: + case RouletteBetTypes.SIXLINE: + case RouletteBetTypes.CORNER: + case RouletteBetTypes.STREET: + return selection.includes(number.toString()); + default: + return false; + } +}; + +const getIsNumberHover = ({ + number, + hoverId, +}: { + number: number; + hoverId: string | null; +}): boolean => { + if (hoverId === null) return false; + + if (hoverId.startsWith('column-')) { + return getIsColumnHover({ number, hoverId }); + } + if (hoverId.startsWith('dozen-')) { + return getIsDozenHover({ number, hoverId }); + } + switch (hoverId as RouletteBetTypes) { + case RouletteBetTypes.LOW: + return number >= 1 && number <= 18; + case RouletteBetTypes.HIGH: + return number >= 19 && number <= 36; + case RouletteBetTypes.EVEN: + return number % 2 === 0; + case RouletteBetTypes.ODD: + return number % 2 !== 0; + case RouletteBetTypes.RED: + return redNumbers.includes(number.toString()); + case RouletteBetTypes.BLACK: + return blackNumbers.includes(number.toString()); + default: + return checkIsChipHover({ number, hoverId }); + } +}; + +export { getIsNumberHover }; diff --git a/apps/frontend/src/features/games/roulette/utils/shouldRender.ts b/apps/frontend/src/features/games/roulette/utils/shouldRender.ts new file mode 100644 index 0000000..43e2208 --- /dev/null +++ b/apps/frontend/src/features/games/roulette/utils/shouldRender.ts @@ -0,0 +1,30 @@ +const noTopRender = [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36]; +const noRightRender = [34, 35, 36]; +const bottomRender = [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34]; +const noCornerBetRender = [...noTopRender, 34, 35]; +const sixLinesBetRender = bottomRender.slice(0, bottomRender.length - 1); +const topRightDobuleStreetRender = noTopRender.slice(0, noTopRender.length - 1); + +const shouldRenderCornerBet = (number: number): boolean => + !noCornerBetRender.includes(number); + +export const shouldRenderTop = (currentNumber: number): boolean => + !noTopRender.includes(currentNumber); + +export const shouldRenderRight = (currentNumber: number): boolean => + !noRightRender.includes(currentNumber); + +export const shouldRenderBottom = (currentNumber: number): boolean => + bottomRender.includes(currentNumber); + +export const shouldRenderSixLineBet = (currentNumber: number): boolean => + sixLinesBetRender.includes(currentNumber); + +export const shouldRenderTopStreet = (currentNumber: number): boolean => + noTopRender.includes(currentNumber); + +export const shouldRenderTopRightDoubleStreet = ( + currentNumber: number +): boolean => topRightDobuleStreetRender.includes(currentNumber); + +export { shouldRenderCornerBet }; diff --git a/apps/frontend/src/features/home/components/CasinoStats.tsx b/apps/frontend/src/features/home/components/CasinoStats.tsx new file mode 100644 index 0000000..5286b00 --- /dev/null +++ b/apps/frontend/src/features/home/components/CasinoStats.tsx @@ -0,0 +1,51 @@ +import { Users, Coins, TrendingUp, Timer } from 'lucide-react'; +import { Card } from '@/components/ui/card'; + +const stats = [ + { + title: 'Active Players', + value: '1,234', + icon: Users, + change: '+12%', + }, + { + title: 'Total Winnings', + value: '1.2M', + icon: Coins, + change: '+5%', + }, + { + title: 'House Edge', + value: '1.5%', + icon: TrendingUp, + change: 'Fixed', + }, + { + title: 'Average Session', + value: '45m', + icon: Timer, + change: '+8%', + }, +]; + +export function CasinoStats(): JSX.Element { + return ( +
+

Casino Statistics

+
+ {stats.map(stat => ( + +
+
+

{stat.title}

+

{stat.value}

+

{stat.change}

+
+ +
+
+ ))} +
+
+ ); +} diff --git a/apps/frontend/src/features/home/components/FeaturedGames.tsx b/apps/frontend/src/features/home/components/FeaturedGames.tsx new file mode 100644 index 0000000..a7993bf --- /dev/null +++ b/apps/frontend/src/features/home/components/FeaturedGames.tsx @@ -0,0 +1,63 @@ +import { Link } from '@tanstack/react-router'; +import { Card } from '@/components/ui/card'; + +const featuredGames = [ + { + id: 'dice', + name: 'Dice', + description: 'Roll the dice and test your luck', + image: '/games/dice.png', + }, + { + id: 'roulette', + name: 'Roulette', + description: 'Classic casino roulette with multiple betting options', + image: '/games/roulette.png', + }, + { + id: 'mines', + name: 'Mines', + description: 'Avoid the mines and collect your rewards', + image: '/games/mines.png', + }, + { + id: 'plinkoo', + name: 'Plinkoo', + description: 'Watch the ball bounce and win big', + image: '/games/plinkoo.png', + }, +]; + +export function FeaturedGames(): JSX.Element { + return ( +
+

Featured Games

+
+ {featuredGames.map(game => ( + + +
+ {game.name} +
+
+

{game.name}

+

+ {game.description} +

+
+
+ + ))} +
+
+ ); +} diff --git a/apps/frontend/src/features/my-bets/columns.tsx b/apps/frontend/src/features/my-bets/columns.tsx new file mode 100644 index 0000000..17c783f --- /dev/null +++ b/apps/frontend/src/features/my-bets/columns.tsx @@ -0,0 +1,113 @@ +import type { PaginatedBetData } from '@repo/common/types'; +import type { ColumnDef } from '@tanstack/react-table'; +import { BadgeDollarSign, ListChecksIcon } from 'lucide-react'; +import { Link } from '@tanstack/react-router'; +import { format, isValid } from 'date-fns'; +import { GAME_VALUES_MAPPING } from '@/const/games'; +import { cn } from '@/lib/utils'; + +export const columns: ColumnDef[] = [ + { + header: 'Game', + accessorKey: 'game', + cell: ({ row }) => { + const game = + GAME_VALUES_MAPPING[ + row.original.game as keyof typeof GAME_VALUES_MAPPING + ]; + + return ( + +
+ {'icon' in game && ( + + )} + {game.label} +
+ + ); + }, + }, + { + header: 'Bet ID', + accessorKey: 'betId', + cell: ({ row }) => { + return ( +
+ + {row.original.betId} +
+ ); + }, + }, + + { + header: 'Date', + accessorKey: 'date', + cell: ({ row }) => { + // Format the date using date-fns + const date = new Date(row.original.date); + const formattedDate = isValid(date) + ? format(date, 'h:mm a M/d/yyyy') + : String(row.original.date); + + return ( +
{formattedDate}
+ ); + }, + meta: { + alignment: 'right', + }, + }, + { + header: 'Bet Amount', + accessorKey: 'betAmount', + cell: ({ row }) => { + return ( +
+ {row.original.betAmount.toFixed(2)}{' '} + +
+ ); + }, + meta: { + alignment: 'right', + }, + }, + { + header: 'Multiplier', + accessorKey: 'payoutMultiplier', + cell: ({ row }) => { + return ( +
+ {row.original.payoutMultiplier.toFixed(2)}x +
+ ); + }, + meta: { + alignment: 'right', + }, + }, + { + header: 'Payout', + accessorKey: 'payout', + cell: ({ row }) => { + return ( +
0, + } + )} + > + {row.original.payout.toFixed(2)}{' '} + +
+ ); + }, + meta: { + alignment: 'right', + }, + }, +]; diff --git a/apps/frontend/src/features/my-bets/index.tsx b/apps/frontend/src/features/my-bets/index.tsx new file mode 100644 index 0000000..642599b --- /dev/null +++ b/apps/frontend/src/features/my-bets/index.tsx @@ -0,0 +1,42 @@ +import { useQuery } from '@tanstack/react-query'; +import { NotepadTextIcon } from 'lucide-react'; +import { useState } from 'react'; +import { fetchUserBetHistory } from '@/api/user'; +import { CommonDataTable } from '@/components/ui/common-data-table'; +import { columns } from './columns'; + +function MyBets(): JSX.Element { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data } = useQuery({ + queryKey: ['my-bets', pagination], + queryFn: () => + fetchUserBetHistory({ + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + }), + placeholderData: prev => prev, + }); + + return ( +
+
+ +

My Bets

+
+ +
+ ); +} + +export default MyBets; diff --git a/apps/frontend/src/features/provaly-fair/ProvablyFairCalculation.tsx b/apps/frontend/src/features/provaly-fair/ProvablyFairCalculation.tsx new file mode 100644 index 0000000..0c82225 --- /dev/null +++ b/apps/frontend/src/features/provaly-fair/ProvablyFairCalculation.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react'; +import { Games, GAMES_DROPDOWN_OPTIONS, type Game } from '@/const/games'; +import CommonSelect from '@/components/ui/common-select'; +import type { VerificationInputsState } from '../games/common/components/fairness-modal/VerificationInputs'; +import VerificationInputs from '../games/common/components/fairness-modal/VerificationInputs'; +import VerificationResult from '../games/common/components/fairness-modal/VerificationResult'; +import DiceResultBreakdown from '../games/dice/components/DiceResultBreakdown'; +import RouletteResultBreakdown from '../games/roulette/components/RouletteResultBreakdown'; +import MinesResultBreakdown from '../games/mines/components/MinesResultBreakdown'; +import KenoResultBreakdown from '../games/keno/components/KenoResultBreakdown'; + +function ProvablyFairCalculation(): JSX.Element { + const [outcome, setOutcome] = useState(null); + const [selectedGame, setSelectedGame] = useState( + GAMES_DROPDOWN_OPTIONS[0].value + ); + const [verificationInputs, setVerificationInputs] = + useState(null); + + const getGameBreakdown = (): JSX.Element => { + switch (selectedGame) { + case Games.DICE: + return ( + + ); + + case Games.ROULETTE: + return ( + + ); + case Games.MINES: + return ( + + ); + case Games.KENO: + return ( + + ); + default: + return
Unknown game
; + } + }; + return ( +
+
+ { + setSelectedGame(value as Game); + }} + options={GAMES_DROPDOWN_OPTIONS} + value={selectedGame} + /> + +
+
+ +
+
{getGameBreakdown()}
+
+ ); +} + +export default ProvablyFairCalculation; diff --git a/apps/frontend/src/features/provaly-fair/UnhashServerSeed.tsx b/apps/frontend/src/features/provaly-fair/UnhashServerSeed.tsx new file mode 100644 index 0000000..6be93b1 --- /dev/null +++ b/apps/frontend/src/features/provaly-fair/UnhashServerSeed.tsx @@ -0,0 +1,100 @@ +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { AlertCircleIcon } from 'lucide-react'; +import { Label } from '@/components/ui/label'; +import InputWithIcon from '@/common/forms/components/InputWithIcon'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { fetchRevealedServerSeed } from '@/api/user'; + +function UnhashServerSeed(): JSX.Element { + const [hashedServerSeed, setHashedServerSeed] = useState(''); + const [revealedServerSeed, setRevealedServerSeed] = useState( + null + ); + + const { + mutate: revealServerSeed, + isPending: isRevealing, + isError, + error, + } = useMutation({ + mutationFn: async () => { + setRevealedServerSeed(null); + if (!hashedServerSeed || hashedServerSeed.length !== 64) { + throw new Error('Hashed server seed must be 64 characters long'); + } + const apiResponse = await fetchRevealedServerSeed(hashedServerSeed); + if (!apiResponse.data.serverSeed) { + throw new Error('Server seed not found'); + } + return apiResponse; + }, + onSuccess: data => { + setRevealedServerSeed(data.data.serverSeed); + }, + }); + return ( +
+
+ +
+
+ { + setHashedServerSeed(e.target.value); + }} + value={hashedServerSeed} + wrapperClassName={cn( + 'bg-brand-stronger border-brand-weaker shadow-none w-full pr-0 h-8 rounded-r-none', + { + 'border-red-500': isError, + } + )} + /> +
+ + +
+ {isError ? ( +
+ + {error.message} +
+ ) : null} +
+
+ +
+
+ +
+
+
+
+ ); +} + +export default UnhashServerSeed; diff --git a/apps/frontend/src/features/provaly-fair/index.tsx b/apps/frontend/src/features/provaly-fair/index.tsx new file mode 100644 index 0000000..1e550b9 --- /dev/null +++ b/apps/frontend/src/features/provaly-fair/index.tsx @@ -0,0 +1,67 @@ +import { Link, Outlet, useLocation } from '@tanstack/react-router'; +import { ScaleIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const provablyFairRoutes = [ + { + label: 'Overview', + path: '/provably-fair', + }, + { + label: 'Implementation', + path: '/provably-fair/implementation', + }, + { + label: 'Conversions', + path: '/provably-fair/conversions', + }, + { + label: 'Game Events', + path: '/provably-fair/game-events', + }, + { + label: 'Unhash Server Seed', + path: '/provably-fair/unhash-server-seed', + }, + { + label: 'Calculation', + path: '/provably-fair/calculation', + }, +]; + +function ProvablyFair(): JSX.Element { + const { pathname } = useLocation(); + + return ( +
+
+ +

Provably Fair

+
+
+
+ {provablyFairRoutes.map(route => ( + +

+ {route.label} +

+ + ))} +
+
+ +
+
+
+ ); +} + +export default ProvablyFair; diff --git a/apps/frontend/src/index.css b/apps/frontend/src/index.css new file mode 100644 index 0000000..a63792e --- /dev/null +++ b/apps/frontend/src/index.css @@ -0,0 +1,125 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + /* Hide number input spinners */ + input[type='number']::-webkit-inner-spin-button, + input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + input[type='number'] { + -moz-appearance: textfield; + } + + @layer base { + /* WebKit Scrollbar (Chrome, Edge, Safari) */ + *::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + *::-webkit-scrollbar-track { + background: transparent; + border-radius: 10px; + } + + *::-webkit-scrollbar-thumb { + background: #7c3aed; + border-radius: 10px; + } + + *::-webkit-scrollbar-thumb:hover { + background: #a78bfa; + } + + *::-webkit-scrollbar-button { + width: 0; + height: 0; + display: none; + } + + /* Firefox Scrollbar */ + * { + scrollbar-width: thin; + scrollbar-color: #2f4553 transparent; + } + + .no-scrollbar { + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE & Edge */ + } + + .no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome, Safari */ + } + } + + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --background: 206, 53%, 12%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 206, 36%, 16%; + --secondary-light: 199, 35%, 19%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --input-disabled: 204, 29%, 25%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground font-custom; + } +} diff --git a/apps/frontend/src/lib/crypto.ts b/apps/frontend/src/lib/crypto.ts new file mode 100644 index 0000000..8a7da45 --- /dev/null +++ b/apps/frontend/src/lib/crypto.ts @@ -0,0 +1,124 @@ +import { range } from 'lodash'; +import chunk from 'lodash/chunk'; + +export const generateRandomString = (length = 10): string => { + const array = new Uint8Array(length); + return btoa( + String.fromCharCode.apply(null, Array.from(crypto.getRandomValues(array))) + ) + .replace(/[^a-zA-Z0-9]/g, '') // Remove non-alphanumeric characters + .slice(0, length); // Ensure exact length +}; + +export const getHmacSeed = async ( + seed: string, + message: string +): Promise => { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(seed), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + const signature = await crypto.subtle.sign( + 'HMAC', + key, + encoder.encode(message) + ); + return Array.from(new Uint8Array(signature)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +}; + +export const getHmacBuffer = async ( + seed: string, + message: string +): Promise => { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(seed), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + return crypto.subtle.sign('HMAC', key, encoder.encode(message)); +}; + +export async function byteGenerator( + seed: string, + message: string, + rounds: number +): Promise { + const promises: Promise[] = []; + + for (let i = 0; i < rounds; i++) { + promises.push( + getHmacBuffer(seed, `${message}:${i}`).then(buf => new Uint8Array(buf)) + ); + } + + const buffers = await Promise.all(promises); + return buffers.flatMap(buffer => Array.from(buffer)); +} + +export const getGeneratedFloats = async ({ + count, + seed, + message, +}: { + count: number; + seed: string; + message: string; +}): Promise => { + const bytesNeeded = count * 4; + const rounds = Math.ceil(bytesNeeded / 32); // Each HMAC buffer gives 32 bytes + + const bytes = await byteGenerator(seed, message, rounds); + const selectedBytes = bytes.slice(0, bytesNeeded); + + return chunk(selectedBytes, 4).map(bytesChunk => + bytesChunk.reduce((result, value, i) => { + const divider = 256 ** (i + 1); + return result + value / divider; + }, 0) + ); +}; + +export const getFisherYatesShuffle = ({ + gameEvents, + stopCount, + totalEventsPossible, +}: { + gameEvents: number[]; + stopCount: number; + totalEventsPossible: number; +}): { array: number[]; chosenIndex: number }[] => { + if (gameEvents.length === 0) { + return []; + } + let eventNumbers = range(totalEventsPossible); + const outcomes = []; + const result: { array: number[]; chosenIndex: number }[] = [ + { array: [...eventNumbers], chosenIndex: gameEvents[0] }, + ]; + for (let i = 0; i < totalEventsPossible; i++) { + const chosenIndex = gameEvents[i]; + outcomes.push(eventNumbers[chosenIndex]); + if (outcomes.length === stopCount) { + break; + } + + eventNumbers = [ + ...eventNumbers.slice(0, chosenIndex), + ...eventNumbers.slice(chosenIndex + 1), + ]; + result.push({ + array: [...outcomes, ...eventNumbers], + chosenIndex: outcomes.length + gameEvents[i + 1], + }); + } + return result; +}; diff --git a/apps/frontend/src/lib/formatters.ts b/apps/frontend/src/lib/formatters.ts new file mode 100644 index 0000000..d0cd817 --- /dev/null +++ b/apps/frontend/src/lib/formatters.ts @@ -0,0 +1,64 @@ +export function formatCompactNumber(value: number, decimalPlaces = 10): string { + if (value === 0) return '0'; + + const absValue = Math.abs(value); + const sign = value < 0 ? '-' : ''; + + // Define thresholds and suffixes + const tiers = [ + { threshold: 1e12, suffix: 'T' }, + { threshold: 1e9, suffix: 'B' }, + { threshold: 1e6, suffix: 'M' }, + { threshold: 1e3, suffix: 'K' }, + { threshold: 1, suffix: '' }, + ]; + + // Find the appropriate tier + const tier = + tiers.find(t => absValue >= t.threshold) ?? tiers[tiers.length - 1]; + + // Calculate the scaled value + const scaledValue = absValue / tier.threshold; + + // Dynamically determine decimal places based on actual value + const formattedValue = + scaledValue % 1 === 0 + ? scaledValue.toFixed(0) + : scaledValue.toPrecision(decimalPlaces); + + // Remove trailing zeros while keeping at least one decimal if applicable + const trimmedValue = formattedValue + .replace(/(?\.\d*?[1-9])0+$/, '$') + .replace(/\.0+$/, ''); + + return `${sign}${trimmedValue}${tier.suffix}`; +} + +export function getYellowToRedColor(value: number, min = 0, max = 100): string { + // Ensure value is within bounds + const bounded = Math.max(min, Math.min(max, value)); + + // Calculate how far along the gradient we are (0 to 1) + const ratio = (bounded - min) / (max - min); + + // Start with yellow (255, 206, 0) and transition to dark red + // Gradually lower green from 206 to 0 + const green = Math.floor(206 * (1 - ratio)); + + // Also gradually lower red from 255 to 180 for a deeper red at high values + const red = Math.floor(255 - ratio * 75); + + return `rgb(${red}, ${green}, 0)`; +} + +export function formatColorizedNumber( + value: number, + min = 0, + max = 100, + decimalPlaces = 1 +): { formatted: string; color: string } { + return { + formatted: formatCompactNumber(value, decimalPlaces), + color: getYellowToRedColor(value, min, max), + }; +} diff --git a/apps/frontend/src/lib/utils.ts b/apps/frontend/src/lib/utils.ts new file mode 100644 index 0000000..a7c2663 --- /dev/null +++ b/apps/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)); +} diff --git a/apps/frontend/src/lib/verificationOutcomes.ts b/apps/frontend/src/lib/verificationOutcomes.ts new file mode 100644 index 0000000..284c1d8 --- /dev/null +++ b/apps/frontend/src/lib/verificationOutcomes.ts @@ -0,0 +1,121 @@ +import { NO_OF_TILES } from '@repo/common/game-utils/mines/constants.js'; +import { + convertFloatsToGameEvents, + calculateMines, +} from '@repo/common/game-utils/mines/utils.js'; +import { NO_OF_TILES_KENO } from '@repo/common/game-utils/keno/constants.js'; +import { calculateSelectedGems } from '@repo/common/game-utils/keno/utils.js'; +import { Games } from '@/const/games'; +import type { Game } from '@/const/games'; +import { getGeneratedFloats } from './crypto'; + +interface MinesGameMeta { + minesCount: number; +} +export type GameMeta = MinesGameMeta | undefined; + +const diceVerificationOutcomes = async ({ + clientSeed, + serverSeed, + nonce, +}: { + clientSeed: string; + serverSeed: string; + nonce: string; +}): Promise => { + const [float] = await getGeneratedFloats({ + count: 1, + seed: serverSeed, + message: `${clientSeed}:${nonce}`, + }); + const result = (float * 10001) / 100; + return result.toFixed(2); +}; + +const minesVerificationOutcomes = async ({ + clientSeed, + serverSeed, + nonce, + meta, +}: { + clientSeed: string; + serverSeed: string; + nonce: string; + meta?: MinesGameMeta; +}): Promise => { + const minesCount = meta?.minesCount ?? 3; + const floats = await getGeneratedFloats({ + count: NO_OF_TILES - 1, + seed: serverSeed, + message: `${clientSeed}:${nonce}`, + }); + const gameEvents = convertFloatsToGameEvents(floats, NO_OF_TILES); + const mines = calculateMines(gameEvents, minesCount); + return mines; +}; + +const rouletteVerificationOutcomes = async ({ + clientSeed, + serverSeed, + nonce, +}: { + clientSeed: string; + serverSeed: string; + nonce: string; +}): Promise => { + const [float] = await getGeneratedFloats({ + count: 1, + seed: serverSeed, + message: `${clientSeed}:${nonce}`, + }); + const result = Math.floor(float * 37); + return result.toString(); +}; + +const kenoVerificationOutcomes = async ({ + clientSeed, + serverSeed, + nonce, +}: { + clientSeed: string; + serverSeed: string; + nonce: string; +}): Promise => { + const floats = await getGeneratedFloats({ + count: 10, + seed: serverSeed, + message: `${clientSeed}:${nonce}`, + }); + const gameEvents = convertFloatsToGameEvents(floats, NO_OF_TILES_KENO); + const drawnNumbers = calculateSelectedGems(gameEvents, 10).map( + num => num + 1 + ); + return drawnNumbers; +}; + +export const getVerificationOutcome = async ({ + game, + clientSeed, + serverSeed, + nonce, + meta, +}: { + game: Game; + clientSeed: string; + serverSeed: string; + nonce: string; + meta?: GameMeta; +}): Promise => { + switch (game) { + case Games.DICE: + return diceVerificationOutcomes({ clientSeed, serverSeed, nonce }); + case Games.ROULETTE: + return rouletteVerificationOutcomes({ clientSeed, serverSeed, nonce }); + case Games.MINES: + return minesVerificationOutcomes({ clientSeed, serverSeed, nonce, meta }); + case Games.KENO: + return kenoVerificationOutcomes({ clientSeed, serverSeed, nonce }); + default: + return ''; + } +}; diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx new file mode 100644 index 0000000..fdaa118 --- /dev/null +++ b/apps/frontend/src/main.tsx @@ -0,0 +1,11 @@ +import { createRoot } from 'react-dom/client'; +import App from './app'; +import './index.css'; + +const el = document.getElementById('root'); +if (el) { + const root = createRoot(el); + root.render(); +} else { + throw new Error('Could not find root element'); +} diff --git a/apps/frontend/src/routeTree.gen.ts b/apps/frontend/src/routeTree.gen.ts new file mode 100644 index 0000000..13d3bdb --- /dev/null +++ b/apps/frontend/src/routeTree.gen.ts @@ -0,0 +1,417 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root'; +import { Route as PublicImport } from './routes/_public'; +import { Route as ProtectedImport } from './routes/_protected'; +import { Route as PublicProvablyFairImport } from './routes/_public/provably-fair'; +import { Route as PublicLoginImport } from './routes/_public/login'; +import { Route as ProtectedCasinoImport } from './routes/_protected/casino'; +import { Route as PublicProvablyFairUnhashServerSeedImport } from './routes/_public/provably-fair/unhash-server-seed'; +import { Route as PublicProvablyFairCalculationImport } from './routes/_public/provably-fair/calculation'; +import { Route as ProtectedCasinoMyBetsImport } from './routes/_protected/casino/my-bets'; +import { Route as ProtectedCasinoHomeImport } from './routes/_protected/casino/home'; +import { Route as ProtectedCasinoGamesImport } from './routes/_protected/casino/games'; +import { Route as ProtectedCasinoGamesGameIdImport } from './routes/_protected/casino/games/$gameId'; + +// Create/Update Routes + +const PublicRoute = PublicImport.update({ + id: '/_public', + getParentRoute: () => rootRoute, +} as any); + +const ProtectedRoute = ProtectedImport.update({ + id: '/_protected', + getParentRoute: () => rootRoute, +} as any); + +const PublicProvablyFairRoute = PublicProvablyFairImport.update({ + id: '/provably-fair', + path: '/provably-fair', + getParentRoute: () => PublicRoute, +} as any); + +const PublicLoginRoute = PublicLoginImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => PublicRoute, +} as any); + +const ProtectedCasinoRoute = ProtectedCasinoImport.update({ + id: '/casino', + path: '/casino', + getParentRoute: () => ProtectedRoute, +} as any); + +const PublicProvablyFairUnhashServerSeedRoute = + PublicProvablyFairUnhashServerSeedImport.update({ + id: '/unhash-server-seed', + path: '/unhash-server-seed', + getParentRoute: () => PublicProvablyFairRoute, + } as any); + +const PublicProvablyFairCalculationRoute = + PublicProvablyFairCalculationImport.update({ + id: '/calculation', + path: '/calculation', + getParentRoute: () => PublicProvablyFairRoute, + } as any); + +const ProtectedCasinoMyBetsRoute = ProtectedCasinoMyBetsImport.update({ + id: '/my-bets', + path: '/my-bets', + getParentRoute: () => ProtectedCasinoRoute, +} as any); + +const ProtectedCasinoHomeRoute = ProtectedCasinoHomeImport.update({ + id: '/home', + path: '/home', + getParentRoute: () => ProtectedCasinoRoute, +} as any); + +const ProtectedCasinoGamesRoute = ProtectedCasinoGamesImport.update({ + id: '/games', + path: '/games', + getParentRoute: () => ProtectedCasinoRoute, +} as any); + +const ProtectedCasinoGamesGameIdRoute = ProtectedCasinoGamesGameIdImport.update( + { + id: '/$gameId', + path: '/$gameId', + getParentRoute: () => ProtectedCasinoGamesRoute, + } as any +); + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/_protected': { + id: '/_protected'; + path: ''; + fullPath: ''; + preLoaderRoute: typeof ProtectedImport; + parentRoute: typeof rootRoute; + }; + '/_public': { + id: '/_public'; + path: ''; + fullPath: ''; + preLoaderRoute: typeof PublicImport; + parentRoute: typeof rootRoute; + }; + '/_protected/casino': { + id: '/_protected/casino'; + path: '/casino'; + fullPath: '/casino'; + preLoaderRoute: typeof ProtectedCasinoImport; + parentRoute: typeof ProtectedImport; + }; + '/_public/login': { + id: '/_public/login'; + path: '/login'; + fullPath: '/login'; + preLoaderRoute: typeof PublicLoginImport; + parentRoute: typeof PublicImport; + }; + '/_public/provably-fair': { + id: '/_public/provably-fair'; + path: '/provably-fair'; + fullPath: '/provably-fair'; + preLoaderRoute: typeof PublicProvablyFairImport; + parentRoute: typeof PublicImport; + }; + '/_protected/casino/games': { + id: '/_protected/casino/games'; + path: '/games'; + fullPath: '/casino/games'; + preLoaderRoute: typeof ProtectedCasinoGamesImport; + parentRoute: typeof ProtectedCasinoImport; + }; + '/_protected/casino/home': { + id: '/_protected/casino/home'; + path: '/home'; + fullPath: '/casino/home'; + preLoaderRoute: typeof ProtectedCasinoHomeImport; + parentRoute: typeof ProtectedCasinoImport; + }; + '/_protected/casino/my-bets': { + id: '/_protected/casino/my-bets'; + path: '/my-bets'; + fullPath: '/casino/my-bets'; + preLoaderRoute: typeof ProtectedCasinoMyBetsImport; + parentRoute: typeof ProtectedCasinoImport; + }; + '/_public/provably-fair/calculation': { + id: '/_public/provably-fair/calculation'; + path: '/calculation'; + fullPath: '/provably-fair/calculation'; + preLoaderRoute: typeof PublicProvablyFairCalculationImport; + parentRoute: typeof PublicProvablyFairImport; + }; + '/_public/provably-fair/unhash-server-seed': { + id: '/_public/provably-fair/unhash-server-seed'; + path: '/unhash-server-seed'; + fullPath: '/provably-fair/unhash-server-seed'; + preLoaderRoute: typeof PublicProvablyFairUnhashServerSeedImport; + parentRoute: typeof PublicProvablyFairImport; + }; + '/_protected/casino/games/$gameId': { + id: '/_protected/casino/games/$gameId'; + path: '/$gameId'; + fullPath: '/casino/games/$gameId'; + preLoaderRoute: typeof ProtectedCasinoGamesGameIdImport; + parentRoute: typeof ProtectedCasinoGamesImport; + }; + } +} + +// Create and export the route tree + +interface ProtectedCasinoGamesRouteChildren { + ProtectedCasinoGamesGameIdRoute: typeof ProtectedCasinoGamesGameIdRoute; +} + +const ProtectedCasinoGamesRouteChildren: ProtectedCasinoGamesRouteChildren = { + ProtectedCasinoGamesGameIdRoute: ProtectedCasinoGamesGameIdRoute, +}; + +const ProtectedCasinoGamesRouteWithChildren = + ProtectedCasinoGamesRoute._addFileChildren(ProtectedCasinoGamesRouteChildren); + +interface ProtectedCasinoRouteChildren { + ProtectedCasinoGamesRoute: typeof ProtectedCasinoGamesRouteWithChildren; + ProtectedCasinoHomeRoute: typeof ProtectedCasinoHomeRoute; + ProtectedCasinoMyBetsRoute: typeof ProtectedCasinoMyBetsRoute; +} + +const ProtectedCasinoRouteChildren: ProtectedCasinoRouteChildren = { + ProtectedCasinoGamesRoute: ProtectedCasinoGamesRouteWithChildren, + ProtectedCasinoHomeRoute: ProtectedCasinoHomeRoute, + ProtectedCasinoMyBetsRoute: ProtectedCasinoMyBetsRoute, +}; + +const ProtectedCasinoRouteWithChildren = ProtectedCasinoRoute._addFileChildren( + ProtectedCasinoRouteChildren +); + +interface ProtectedRouteChildren { + ProtectedCasinoRoute: typeof ProtectedCasinoRouteWithChildren; +} + +const ProtectedRouteChildren: ProtectedRouteChildren = { + ProtectedCasinoRoute: ProtectedCasinoRouteWithChildren, +}; + +const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren( + ProtectedRouteChildren +); + +interface PublicProvablyFairRouteChildren { + PublicProvablyFairCalculationRoute: typeof PublicProvablyFairCalculationRoute; + PublicProvablyFairUnhashServerSeedRoute: typeof PublicProvablyFairUnhashServerSeedRoute; +} + +const PublicProvablyFairRouteChildren: PublicProvablyFairRouteChildren = { + PublicProvablyFairCalculationRoute: PublicProvablyFairCalculationRoute, + PublicProvablyFairUnhashServerSeedRoute: + PublicProvablyFairUnhashServerSeedRoute, +}; + +const PublicProvablyFairRouteWithChildren = + PublicProvablyFairRoute._addFileChildren(PublicProvablyFairRouteChildren); + +interface PublicRouteChildren { + PublicLoginRoute: typeof PublicLoginRoute; + PublicProvablyFairRoute: typeof PublicProvablyFairRouteWithChildren; +} + +const PublicRouteChildren: PublicRouteChildren = { + PublicLoginRoute: PublicLoginRoute, + PublicProvablyFairRoute: PublicProvablyFairRouteWithChildren, +}; + +const PublicRouteWithChildren = + PublicRoute._addFileChildren(PublicRouteChildren); + +export interface FileRoutesByFullPath { + '': typeof PublicRouteWithChildren; + '/casino': typeof ProtectedCasinoRouteWithChildren; + '/login': typeof PublicLoginRoute; + '/provably-fair': typeof PublicProvablyFairRouteWithChildren; + '/casino/games': typeof ProtectedCasinoGamesRouteWithChildren; + '/casino/home': typeof ProtectedCasinoHomeRoute; + '/casino/my-bets': typeof ProtectedCasinoMyBetsRoute; + '/provably-fair/calculation': typeof PublicProvablyFairCalculationRoute; + '/provably-fair/unhash-server-seed': typeof PublicProvablyFairUnhashServerSeedRoute; + '/casino/games/$gameId': typeof ProtectedCasinoGamesGameIdRoute; +} + +export interface FileRoutesByTo { + '': typeof PublicRouteWithChildren; + '/casino': typeof ProtectedCasinoRouteWithChildren; + '/login': typeof PublicLoginRoute; + '/provably-fair': typeof PublicProvablyFairRouteWithChildren; + '/casino/games': typeof ProtectedCasinoGamesRouteWithChildren; + '/casino/home': typeof ProtectedCasinoHomeRoute; + '/casino/my-bets': typeof ProtectedCasinoMyBetsRoute; + '/provably-fair/calculation': typeof PublicProvablyFairCalculationRoute; + '/provably-fair/unhash-server-seed': typeof PublicProvablyFairUnhashServerSeedRoute; + '/casino/games/$gameId': typeof ProtectedCasinoGamesGameIdRoute; +} + +export interface FileRoutesById { + __root__: typeof rootRoute; + '/_protected': typeof ProtectedRouteWithChildren; + '/_public': typeof PublicRouteWithChildren; + '/_protected/casino': typeof ProtectedCasinoRouteWithChildren; + '/_public/login': typeof PublicLoginRoute; + '/_public/provably-fair': typeof PublicProvablyFairRouteWithChildren; + '/_protected/casino/games': typeof ProtectedCasinoGamesRouteWithChildren; + '/_protected/casino/home': typeof ProtectedCasinoHomeRoute; + '/_protected/casino/my-bets': typeof ProtectedCasinoMyBetsRoute; + '/_public/provably-fair/calculation': typeof PublicProvablyFairCalculationRoute; + '/_public/provably-fair/unhash-server-seed': typeof PublicProvablyFairUnhashServerSeedRoute; + '/_protected/casino/games/$gameId': typeof ProtectedCasinoGamesGameIdRoute; +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath; + fullPaths: + | '' + | '/casino' + | '/login' + | '/provably-fair' + | '/casino/games' + | '/casino/home' + | '/casino/my-bets' + | '/provably-fair/calculation' + | '/provably-fair/unhash-server-seed' + | '/casino/games/$gameId'; + fileRoutesByTo: FileRoutesByTo; + to: + | '' + | '/casino' + | '/login' + | '/provably-fair' + | '/casino/games' + | '/casino/home' + | '/casino/my-bets' + | '/provably-fair/calculation' + | '/provably-fair/unhash-server-seed' + | '/casino/games/$gameId'; + id: + | '__root__' + | '/_protected' + | '/_public' + | '/_protected/casino' + | '/_public/login' + | '/_public/provably-fair' + | '/_protected/casino/games' + | '/_protected/casino/home' + | '/_protected/casino/my-bets' + | '/_public/provably-fair/calculation' + | '/_public/provably-fair/unhash-server-seed' + | '/_protected/casino/games/$gameId'; + fileRoutesById: FileRoutesById; +} + +export interface RootRouteChildren { + ProtectedRoute: typeof ProtectedRouteWithChildren; + PublicRoute: typeof PublicRouteWithChildren; +} + +const rootRouteChildren: RootRouteChildren = { + ProtectedRoute: ProtectedRouteWithChildren, + PublicRoute: PublicRouteWithChildren, +}; + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes(); + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/_protected", + "/_public" + ] + }, + "/_protected": { + "filePath": "_protected.tsx", + "children": [ + "/_protected/casino" + ] + }, + "/_public": { + "filePath": "_public.tsx", + "children": [ + "/_public/login", + "/_public/provably-fair" + ] + }, + "/_protected/casino": { + "filePath": "_protected/casino.jsx", + "parent": "/_protected", + "children": [ + "/_protected/casino/games", + "/_protected/casino/home", + "/_protected/casino/my-bets" + ] + }, + "/_public/login": { + "filePath": "_public/login.tsx", + "parent": "/_public" + }, + "/_public/provably-fair": { + "filePath": "_public/provably-fair.tsx", + "parent": "/_public", + "children": [ + "/_public/provably-fair/calculation", + "/_public/provably-fair/unhash-server-seed" + ] + }, + "/_protected/casino/games": { + "filePath": "_protected/casino/games.tsx", + "parent": "/_protected/casino", + "children": [ + "/_protected/casino/games/$gameId" + ] + }, + "/_protected/casino/home": { + "filePath": "_protected/casino/home.tsx", + "parent": "/_protected/casino" + }, + "/_protected/casino/my-bets": { + "filePath": "_protected/casino/my-bets.tsx", + "parent": "/_protected/casino" + }, + "/_public/provably-fair/calculation": { + "filePath": "_public/provably-fair/calculation.tsx", + "parent": "/_public/provably-fair" + }, + "/_public/provably-fair/unhash-server-seed": { + "filePath": "_public/provably-fair/unhash-server-seed.tsx", + "parent": "/_public/provably-fair" + }, + "/_protected/casino/games/$gameId": { + "filePath": "_protected/casino/games/$gameId.tsx", + "parent": "/_protected/casino/games" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/apps/frontend/src/routes/__root.tsx b/apps/frontend/src/routes/__root.tsx new file mode 100644 index 0000000..1bd8250 --- /dev/null +++ b/apps/frontend/src/routes/__root.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'; +import { TanStackRouterDevtools } from '@tanstack/router-devtools'; +import type { AuthState } from '@/features/auth/store/authStore'; +import { useAuthStore } from '@/features/auth/store/authStore'; +import { setupInterceptors } from '@/api/_utils/axiosInstance'; +import { LoginModal } from '@/features/auth/components/LoginModal'; + +interface RouterContext { + authStore: AuthState | undefined; +} + +export const Route = createRootRouteWithContext()({ + component: RootLayout, +}); + +function RootLayout(): JSX.Element { + const { setUser, showLoginModal } = useAuthStore(); + + // Setup interceptors to show login modal on auth errors + React.useEffect(() => { + setupInterceptors({ + authErrCb: () => { + setUser(null); + showLoginModal(); + }, + }); + }, [setUser, showLoginModal]); + + return ( + <> + + + {import.meta.env.DEV ? : null} + + ); +} diff --git a/apps/frontend/src/routes/_protected.tsx b/apps/frontend/src/routes/_protected.tsx new file mode 100644 index 0000000..42b019d --- /dev/null +++ b/apps/frontend/src/routes/_protected.tsx @@ -0,0 +1,38 @@ +import { createFileRoute, Outlet } from '@tanstack/react-router'; +import { QueryClient } from '@tanstack/react-query'; +import { getAuthState } from '@/features/auth/store/authStore'; +import { getUserDetails } from '@/api/auth'; +import { Header } from '@/components/Header'; + +export const Route = createFileRoute('/_protected')({ + async beforeLoad({ context }) { + const { user, showLoginModal } = getAuthState(); + + if (!user) { + try { + // Fetch user details if not already authenticated + const queryClient = new QueryClient(); + const res = await queryClient.fetchQuery({ + queryKey: ['me'], + queryFn: getUserDetails, + retry: false, + }); + // Set user in auth store if fetch succeeds + context.authStore?.setUser(res.data); + } catch (error) { + // Instead of redirecting, show login modal + showLoginModal(); + } + } + }, + component: ProtectedLayout, +}); + +function ProtectedLayout(): JSX.Element { + return ( +
+
+ +
+ ); +} diff --git a/apps/frontend/src/routes/_protected/casino.jsx b/apps/frontend/src/routes/_protected/casino.jsx new file mode 100644 index 0000000..7a54fb0 --- /dev/null +++ b/apps/frontend/src/routes/_protected/casino.jsx @@ -0,0 +1,17 @@ +import { + createFileRoute, + MatchRoute, + Navigate, + Outlet, +} from '@tanstack/react-router'; + +export const Route = createFileRoute('/_protected/casino')({ + component: () => ( + <> + + + + + + ), +}); diff --git a/apps/frontend/src/routes/_protected/casino/games.tsx b/apps/frontend/src/routes/_protected/casino/games.tsx new file mode 100644 index 0000000..8545b62 --- /dev/null +++ b/apps/frontend/src/routes/_protected/casino/games.tsx @@ -0,0 +1,19 @@ +import { + createFileRoute, + MatchRoute, + Navigate, + Outlet, +} from '@tanstack/react-router'; + +export const Route = createFileRoute('/_protected/casino/games')({ + component: () => ( + <> + + , + +
+ +
+ + ), +}); diff --git a/apps/frontend/src/routes/_protected/casino/games/$gameId.tsx b/apps/frontend/src/routes/_protected/casino/games/$gameId.tsx new file mode 100644 index 0000000..f755019 --- /dev/null +++ b/apps/frontend/src/routes/_protected/casino/games/$gameId.tsx @@ -0,0 +1,32 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { Roulette } from '@/features/games/roulette'; +import { Mines } from '@/features/games/mines'; +import { Plinkoo } from '@/features/games/plinkoo'; +import { DiceGame } from '@/features/games/dice'; +import { Keno } from '@/features/games/keno'; +import Blackjack from '@/features/games/blackjack'; + +export const Route = createFileRoute('/_protected/casino/games/$gameId')({ + component: GamePage, +}); + +function GamePage(): JSX.Element { + const { gameId } = Route.useParams(); + + switch (gameId) { + case 'dice': + return ; + case 'roulette': + return ; + case 'mines': + return ; + case 'plinkoo': + return ; + case 'keno': + return ; + case 'blackjack': + return ; + default: + return
Game not found
; + } +} diff --git a/apps/frontend/src/routes/_protected/casino/home.tsx b/apps/frontend/src/routes/_protected/casino/home.tsx new file mode 100644 index 0000000..dd73416 --- /dev/null +++ b/apps/frontend/src/routes/_protected/casino/home.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/_protected/casino/home')({ + component: () => <>Home, +}); diff --git a/apps/frontend/src/routes/_protected/casino/my-bets.tsx b/apps/frontend/src/routes/_protected/casino/my-bets.tsx new file mode 100644 index 0000000..2dc28bd --- /dev/null +++ b/apps/frontend/src/routes/_protected/casino/my-bets.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import MyBets from '@/features/my-bets'; + +export const Route = createFileRoute('/_protected/casino/my-bets')({ + component: MyBets, +}); diff --git a/apps/frontend/src/routes/_public.tsx b/apps/frontend/src/routes/_public.tsx new file mode 100644 index 0000000..3f42402 --- /dev/null +++ b/apps/frontend/src/routes/_public.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { z } from 'zod'; + +export const Route = createFileRoute('/_public')({ + validateSearch: z.object({ + redirect: z.string().optional().catch(''), + }), + // Not redirecting or blocking content - user will see public content regardless of auth state +}); diff --git a/apps/frontend/src/routes/_public/login.tsx b/apps/frontend/src/routes/_public/login.tsx new file mode 100644 index 0000000..2798544 --- /dev/null +++ b/apps/frontend/src/routes/_public/login.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import Login from '@/features/auth'; + +export const Route = createFileRoute('/_public/login')({ + component: Login, +}); diff --git a/apps/frontend/src/routes/_public/provably-fair.tsx b/apps/frontend/src/routes/_public/provably-fair.tsx new file mode 100644 index 0000000..da96fe9 --- /dev/null +++ b/apps/frontend/src/routes/_public/provably-fair.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import ProvablyFair from '@/features/provaly-fair'; + +export const Route = createFileRoute('/_public/provably-fair')({ + component: ProvablyFair, +}); diff --git a/apps/frontend/src/routes/_public/provably-fair/calculation.tsx b/apps/frontend/src/routes/_public/provably-fair/calculation.tsx new file mode 100644 index 0000000..fd8adb6 --- /dev/null +++ b/apps/frontend/src/routes/_public/provably-fair/calculation.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import ProvablyFairCalculation from '@/features/provaly-fair/ProvablyFairCalculation'; + +export const Route = createFileRoute('/_public/provably-fair/calculation')({ + component: ProvablyFairCalculation, +}); diff --git a/apps/frontend/src/routes/_public/provably-fair/unhash-server-seed.tsx b/apps/frontend/src/routes/_public/provably-fair/unhash-server-seed.tsx new file mode 100644 index 0000000..13fae9a --- /dev/null +++ b/apps/frontend/src/routes/_public/provably-fair/unhash-server-seed.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from '@tanstack/react-router'; +import UnhashServerSeed from '@/features/provaly-fair/UnhashServerSeed'; + +export const Route = createFileRoute( + '/_public/provably-fair/unhash-server-seed' +)({ + component: UnhashServerSeed, +}); diff --git a/apps/frontend/src/store/gameSettings.ts b/apps/frontend/src/store/gameSettings.ts new file mode 100644 index 0000000..1e5e76d --- /dev/null +++ b/apps/frontend/src/store/gameSettings.ts @@ -0,0 +1,78 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface GameSettingsState { + // Audio settings + volume: number; + + // Visual settings + animations: boolean; + + // Betting settings + showMaxBetButton: boolean; + instantBet: boolean; + + // Controls + hotkeysEnabled: boolean; + + // Actions + setVolume: (volume: number) => void; + setAnimations: (enabled: boolean) => void; + setShowMaxBetButton: (show: boolean) => void; + setInstantBet: (enabled: boolean) => void; + setHotkeysEnabled: (enabled: boolean) => void; + + // Reset all settings to defaults + resetToDefaults: () => void; +} + +// Default settings +const defaultSettings = { + volume: 100, + animations: true, + showMaxBetButton: false, + instantBet: false, + hotkeysEnabled: false, +}; + +export const useGameSettingsStore = create()( + persist( + set => ({ + // Initial state with defaults + ...defaultSettings, + + // Actions to update individual settings + setVolume: volume => { + // Ensure volume is between 0 and 100 + const validVolume = Math.max(0, Math.min(100, volume)); + set({ volume: validVolume }); + }, + + setAnimations: enabled => { + set({ animations: enabled }); + }, + + setShowMaxBetButton: show => { + set({ showMaxBetButton: show }); + }, + + setInstantBet: enabled => { + set({ instantBet: enabled }); + }, + + setHotkeysEnabled: enabled => { + set({ hotkeysEnabled: enabled }); + }, + + // Reset all settings to default values + resetToDefaults: () => { + set(defaultSettings); + }, + }), + { + name: 'game-settings-storage', // localStorage key + // Sync with localStorage immediately + skipHydration: false, + } + ) +); diff --git a/apps/frontend/src/store/index.ts b/apps/frontend/src/store/index.ts new file mode 100644 index 0000000..f147e88 --- /dev/null +++ b/apps/frontend/src/store/index.ts @@ -0,0 +1 @@ +export { useGameSettingsStore } from './gameSettings'; diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js new file mode 100644 index 0000000..ffe5350 --- /dev/null +++ b/apps/frontend/tailwind.config.js @@ -0,0 +1,234 @@ +const plugin = require('tailwindcss/plugin'); + +const iconTokens = { + 'neutral-weak': '#557086', + 'neutral-weaker': '#2f4553', + 'neutral-weak': '#b1b4d3', + 'neutral-default': '#fff', + 'neutral-strong': '#1a2c38', + 'neutral-stronger': '#0f212e', + 'neutral-strongest': '#071824', +}; + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ['selector', 'class'], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + './index.html', + ], + prefix: '', + theme: { + container: { + center: 'true', + padding: '2rem', + screens: { + xl: '1200px', + lg: '1024px', + md: '768px', + sm: '640px', + }, + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: { + DEFAULT: 'hsl(var(--input))', + disabled: 'hsl(var(--input-disabled))', + }, + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + light: 'hsl(var(--secondary-light))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + chart: { + 1: 'hsl(var(--chart-1))', + 2: 'hsl(var(--chart-2))', + 3: 'hsl(var(--chart-3))', + 4: 'hsl(var(--chart-4))', + 5: 'hsl(var(--chart-5))', + }, + }, + borderWidth: { + 3: '3px', + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + gridTemplateColumns: { + 14: 'repeat(14, minmax(0, 1fr))', + }, + gridColumn: { + span14: 'span 14 / span 14', + }, + keyframes: { + 'accordion-down': { + from: { + height: '0', + }, + to: { + height: 'var(--radix-accordion-content-height)', + }, + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)', + }, + to: { + height: '0', + }, + }, + slideInLeft: { + '0%': { transform: 'translateX(20px)', opacity: 0 }, + '100%': { transform: 'translateX(0)', opacity: 1 }, + }, + slideOutLeft: { + '0%': { transform: 'translateX(0)', opacity: 1 }, + '100%': { transform: 'translateX(-20px)', opacity: 0 }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + slideInLeft: 'slideInLeft 600ms ease-out', + slideOutLeft: 'slideOutLeft 600ms ease-out', + }, + fontFamily: { + custom: ['Montserrat', 'sans-serif'], + }, + }, + backgroundColor: ({ theme }) => ({ + ...theme('colors'), + 'neutral-weak': '#557086', + 'neutral-weaker': '#2f4553', + 'neutral-weak': '#b1b4d3', + 'neutral-default': '#fff', + 'neutral-strong': '#1a2c38', + 'neutral-stronger': '#0f212e', + 'neutral-strongest': '#1a2c38', + 'brand-weakest': '#557086', + 'brand-weaker': '#2f4553', + 'brand-weak': '#213743', + 'brand-default': '#1a2c38', + 'brand-strong': '#1a2c38', + 'brand-stronger': '#0f212e', + 'brand-strongest': '#071824', + 'roulette-red': '#fe2247', + 'roulette-red-hover': '#fe6e86', + 'roulette-black': '#2f4553', + 'roulette-black-hover': '#4b6e84', + 'roulette-green': '#419e3f', + 'roulette-green-hover': '#69c267', + 'keno-selected-tile': '#962EFF', + 'keno-selected-tile-hover': 'rgb(176, 97, 255)', + }), + textColor: ({ theme }) => ({ + 'roulette-red': '#fe2247', + 'roulette-red-hover': '#fe6e86', + 'roulette-black': '#2f4553', + 'roulette-black-hover': '#4b6e84', + 'roulette-green': '#419e3f', + 'roulette-green-hover': '#69c267', + 'neutral-weak': '#557086', + 'neutral-weaker': '#2f4553', + 'neutral-weak': '#b1b4d3', + 'neutral-default': '#fff', + 'neutral-strong': '#1a2c38', + 'neutral-stronger': '#0f212e', + 'neutral-strongest': '#1a2c38', + 'brand-weakest': '#1a2c38', + 'brand-weaker': '#1a2c38', + 'brand-weak': '#1a2c38', + 'brand-default': '#1a2c38', + 'brand-strong': '#1a2c38', + 'brand-stronger': '#1a2c38', + 'brand-strongest': '#071824', + ...theme('colors'), + }), + borderColor: ({ theme }) => ({ + 'roulette-red': '#fe2247', + 'roulette-red-hover': '#fe6e86', + 'roulette-black': '#2f4553', + 'roulette-black-hover': '#4b6e84', + 'roulette-green': '#419e3f', + 'roulette-green-hover': '#69c267', + 'neutral-weak': '#557086', + 'neutral-weaker': '#2f4553', + 'neutral-weak': '#b1b4d3', + 'neutral-default': '#fff', + 'neutral-strong': '#1a2c38', + 'neutral-stronger': '#0f212e', + 'neutral-strongest': '#1a2c38', + 'brand-weakest': '#557086', + 'brand-weaker': '#2f4553', + 'brand-weak': '#213743', + 'brand-default': '#1a2c38', + 'brand-strong': '#1a2c38', + 'brand-stronger': '#0f212e', + 'brand-strongest': '#071824', + ...theme('colors'), + }), + fill: ({ theme }) => ({ + 'neutral-weak': '#557086', + 'neutral-weaker': '#2f4553', + 'neutral-weak': '#b1b4d3', + 'neutral-default': '#fff', + 'neutral-strong': '#1a2c38', + 'neutral-stronger': '#0f212e', + 'neutral-strongest': '#1a2c38', + 'brand-weakest': '#557086', + 'brand-weaker': '#2f4553', + 'brand-weak': '#213743', + 'brand-default': '#1a2c38', + 'brand-strong': '#1a2c38', + 'brand-stronger': '#0f212e', + 'brand-strongest': '#071824', + ...theme('colors'), + }), + }, + plugins: [ + require('tailwindcss-animate'), + plugin(({ matchUtilities }) => { + matchUtilities( + { + icon: value => ({ color: value }), + }, + { + values: { ...iconTokens }, + } + ); + }), + ], +}; diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json new file mode 100644 index 0000000..4935dd4 --- /dev/null +++ b/apps/frontend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "exclude": ["node_modules"], + "extends": "@repo/typescript-config/vite.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@repo/*": ["../../packages/*/src"] + }, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/apps/frontend/tsr.config.json b/apps/frontend/tsr.config.json new file mode 100644 index 0000000..7f36ce5 --- /dev/null +++ b/apps/frontend/tsr.config.json @@ -0,0 +1,3 @@ +{ + "autoCodeSplitting": true +} diff --git a/apps/frontend/turbo.json b/apps/frontend/turbo.json new file mode 100644 index 0000000..52e8c76 --- /dev/null +++ b/apps/frontend/turbo.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "//" + ], + "tasks": { + "build": { + "outputs": [ + "dist/**" + ] + } + } +} diff --git a/apps/frontend/vercel.json b/apps/frontend/vercel.json new file mode 100644 index 0000000..00e7ecc --- /dev/null +++ b/apps/frontend/vercel.json @@ -0,0 +1,3 @@ +{ + "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }] +} diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts new file mode 100644 index 0000000..470abb0 --- /dev/null +++ b/apps/frontend/vite.config.ts @@ -0,0 +1,17 @@ +import path from 'node:path'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; +import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; + +export default defineConfig({ + plugins: [ + TanStackRouterVite({ target: 'react', autoCodeSplitting: true }), + react(), + ], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@repo': path.resolve(__dirname, '../../packages'), + }, + }, +}); diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index f06235c..0000000 --- a/backend/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/backend/package-lock.json b/backend/package-lock.json deleted file mode 100644 index dcb5bac..0000000 --- a/backend/package-lock.json +++ /dev/null @@ -1,816 +0,0 @@ -{ - "name": "backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "backend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "cors": "^2.8.5", - "express": "^4.19.2" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", - "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" - }, - "node_modules/@types/node": { - "version": "20.12.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", - "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - } - } -} diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index ed509b2..0000000 --- a/backend/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "backend", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "build": "tsc -b", - "start": "node dist/index.js" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "cors": "^2.8.5", - "express": "^4.19.2" - } -} diff --git a/backend/src/index.ts b/backend/src/index.ts deleted file mode 100644 index a1206d8..0000000 --- a/backend/src/index.ts +++ /dev/null @@ -1,53 +0,0 @@ - -import express from "express"; -import { outcomes } from "./outcomes"; -import cors from "cors"; - -const app = express(); -app.use(cors()) - -const TOTAL_DROPS = 16; - -const MULTIPLIERS: {[ key: number ]: number} = { - 0: 16, - 1: 9, - 2: 2, - 3: 1.4, - 4: 1.4, - 5: 1.2, - 6: 1.1, - 7: 1, - 8: 0.5, - 9: 1, - 10: 1.1, - 11: 1.2, - 12: 1.4, - 13: 1.4, - 14: 2, - 15: 9, - 16: 16 -} - -app.post("/game", (req, res) => { - let outcome = 0; - const pattern = [] - for (let i = 0; i < TOTAL_DROPS; i++) { - if (Math.random() > 0.5) { - pattern.push("R") - outcome++; - } else { - pattern.push("L") - } - } - - const multiplier = MULTIPLIERS[outcome]; - const possiblieOutcomes = outcomes[outcome]; - - res.send({ - point: possiblieOutcomes[Math.floor(Math.random() * possiblieOutcomes.length || 0)], - multiplier, - pattern - }); -}); - -app.listen(3000) \ No newline at end of file diff --git a/backend/src/outcomes.ts b/backend/src/outcomes.ts deleted file mode 100644 index f60a237..0000000 --- a/backend/src/outcomes.ts +++ /dev/null @@ -1 +0,0 @@ -export const outcomes: {[key: string]: number[]} = { "0": [], "1": [ 3964963.452981615, 3910113.3998412564 ], "2": [ 3980805.7004139693, 3945617.6504109767, 4027628.395823398, 3902115.8620758583, 3938709.5467746584 ], "3": [ 3975554.824601942, 3965805.769610554, 3909279.443666201, 3940971.550465178, 3909606.717374134, 3915484.1741136736, 3977018.430328505, 3979167.5933461944, 3995981.0273005674, 3974177.78840204 ], "4": [ 3943174.7607756723, 3992961.0886867167, 3914511.2798374896, 3950487.300703086, 3973378.3900412438, 4012888.985549594, 4040961.8767680754, 4066503.3857407006, 3944573.7194061875, 3979876.769324002, 4042712.772834604, 4032991.0303322095, 4046340.7919081766, 3912597.9665436875, 4068852.495940549, 4064879.257329362, 3996796.04239161, 4045062.2783860737, 3964680.919169739 ], "5": [ 3953045.1447091424, 3947374.62976226, 3924082.6101653073, 3919085.269354398, 3902650.4008744615, 3934968.1593932374, 4044126.7590222214, 3928499.8807134246, 3913801.9247018984, 3909595.4432100505, 4082827.827013994, 3979739.108665962, 4077651.317785833, 4008030.8883127486, 3950951.6007580766, 3992039.9053288833, 4021810.0928285993, 4052650.560434505, 3994806.267259329, 3959327.3735489477, 3940455.7641962855, 3998822.2807239015, 3998803.9335444313, 4068193.3913483596, 3938798.911585438 ], "6": [ 4065643.7049927213, 3936841.961313155, 3948472.8991447487, 4004510.5975928125, 3933695.6888747592, 4011296.1958215656, 4093232.84383817, 3945658.6170622837, 4063199.5117669366, 4037864.799653558, 3931477.3517858014, 4091381.513010509, 4000895.053297006, 4042867.6535872207, 4090947.938511616, 3989468.333758437, 3943335.764879169, 3947278.536321405, 4022304.817103859, 3902177.8466275427, 3925270.959381573, 3955253.4540312397, 3986641.0060988157, 3927696.2396482667, 4064571.150949869, 3991167.946685552, 3973041.308793569, 3987377.180906899, 3917262.667253392, 4002606.795366179, 4033596.992526079, 3901372.366183016, 4015207.583244224, 3955421.290959922, 3952223.0425123484, 3941774.4498685915, 3977289.3718391117, 4024943.3014183883, 4024885.5052148327, 4016596.7449097126, 3910164.1864616796, 4023400.498352244, 3981421.8628830933, 3913377.3496230906, 4045958.9425667236, 4071139.892029292, 4019862.922309672, 4027992.2300945413, 4030455.1701347437, 4060673.10227606, 3996564.062673036, 4009801.4052053, 4007734.404953163, 4046612.754675019, 3944956.9979153597, 3977382.889196781, 3906636.5132748624, 4080470.0674178666, 3996210.4877184015, 3956216.294023866, 3940040.183231992 ], "7": [ 3926739.9104774813, 4091374.44234272, 4061919.9903071183, 3976066.7555194413, 3948801.1936986246, 4043233.7830772344, 4010011.7658794387, 3936431.4108806592, 3942776.8649452417, 3909995.011479453, 4012272.43979473, 3989907.069429411, 3996182.4336681785, 4078644.79693604, 4081624.0834239917, 4025044.731614778, 4033602.5381773794, 3913189.826642105, 3910500.674962151, 4055296.6588616692, 4005574.8641647273, 4079800.3518520766, 4092763.5236495608, 3952185.4910905147, 3945510.495018459, 3920891.8818843197, 3997101.789672143, 3991974.822516503, 3949265.4371072412, 3933412.4749754136, 3933181.8312838264, 4063875.6616431624, 3998206.7252218956, 3959006.1987530286, 3924067.917601976, 3902914.4459602935, 3905347.098696195, 4000831.565288375, 3944915.3251241, 3930343.481158048, 4025858.616981573, 4026496.026592473, 3948116.019901921, 4067143.737297127, 3995156.000931595, 3905006.3301882823, 4035783.4852589793, 3956461.6106608217, 4032886.6912715673, 3913146.10237042, 3930772.085213345, 3984887.619042549, 4053031.0321973227, 3913395.137097174, 3993579.678508536, 3932427.236196532, 3984279.0886106077 ], "8": [ 4099062.75134143, 4085894.4181278455, 3991123.0115790954, 3973053.5827605873, 3968190.564301313, 3925604.5066868863, 3933898.7590061547, 4089919.7991958153, 4076997.5225973814, 3957630.60529322, 3948999.35996541, 3963938.9455971997, 4044805.7991237757, 3905133.2109927135, 4074463.6876271376, 3939301.0655442886, 4040571.320635691, 4020510.19979044, 3959835.4618981928, 4037241.67248416, 4043105.87901907, 3912654.2409310103, 3929773.262095125, 3950802.527033251, 4068582.4605300324, 3946792.6177569656, 4078475.9982660934, 3972024.763383927, 3947150.677862883, 3963410.9779685168, 3999134.851845996, 3909374.1117644133, 3942761.896008833, 4071253.4107468165, 4050534.50171971, 3988521.4618817912, 3929940.089627246, 4029305.1056314665, 4087943.221841722, 3910909.3079385986, 4046944.0552393594, 4006944.159180551, 4014707.657017377, 3925473.574267122, 4012158.905329344, 4042197.149473071, 3998434.6078570196, 4047267.2747256896, 3964753.3725316986, 3955821.0222197613, 3973475.662585886, 3917189.0280630635, 4027132.7848505056, 3905368.7668914935, 3936654.62186107, 4092566.3229272505, 4026541.0685970024, 4038770.6420815475, 4067262.4257867294, 4050430.5327158393, 3980149.8069138955, 4052184.5678737606, 3942299.598280835, 4079754.687607573, 4021112.5651541506, 3961023.3381184433, 3937025.1424917267, 3964607.486702018, 4001319.0133674755, 3941648.5232227165, 4030587.9685114417, 4044067.1579758436, 4058158.522928313 ], "9": [ 3911530.315770063, 4024711.492410591, 3967652.4297853387, 4098886.3793751886, 4026117.0283389515, 4045045.4095477182, 4034571.220507859, 4088809.303306565, 3900806.968890352, 3913166.9251142726, 4059594.3600833854, 3945137.694311404, 3902668.8160601873, 4054646.2889849013, 4053898.6542759663, 3959251.11275926, 3963475.882565954, 3967968.9310842347, 4075078.929914972, 4035117.4533019722, 4047608.2592268144, 3913024.5010530455, 4081362.0390194473, 4098538.7144543654, 4049336.7774994993, 4056844.5727342237, 3917845.6810319433, 4098332.1779752634, 3979547.7686487637, 4026747.155594485, 3944692.803167993, 3960649.105237204, 4081040.2295870385, 4005698.9658651184, 4074183.694152899, 3976184.3586868607, 4007157.5084493076, 3918927.3398626954, 3918166.0285542854, 3953868.3374998523, 3963648.6249533077, 4065036.1837552087, 3964230.698479104, 3992799.530672317, 3931113.922813188, 4082916.6661583954, 3919236.111874976, 4012743.1541231154, 3900406.2441578982, 4031396.764516756, 4088712.2834741194, 3921570.4946371615, 4077416.64169384, 3962807.6000533635 ], "10": [ 4069582.648305392, 3966300.3577461895, 4047184.7847023425, 3962656.256238744, 3934682.0223851865, 4089620.291559703, 3996605.065672608, 3921656.567101851, 3950930.30704122, 4052733.606190915, 4046762.051641918, 3912718.72211605, 3942094.6698735086, 4017504.735499972, 4016206.1612997893, 4060896.040328729, 4077224.686824909, 3988932.185505723, 4016550.502499315, 3959104.134236025, 3903531.023685199, 3939907.5585800377, 3969464.753065079, 4036549.7059165714, 3938844.715578784, 3985594.4268763512, 4011615.276676018, 3949739.058361909, 4064041.8926257566, 4004767.498301687, 3996411.8026064364, 4035064.3182208547, 3988008.7378418343, 4015638.96642283, 3967068.722994021, 4082965.2856357233, 3951302.134707721, 3948101.1830631103, 3978745.8509503608, 4068638.265329366, 4018433.726155858, 4032765.523475676 ], "11": [ 4055462.593704495, 4027576.362231998, 4011290.7395424685, 4034848.6574270525, 4064298.598636101, 3997022.919190929, 4053625.932623065, 4064234.3514714935, 4075348.9710445153, 4060118.5348266517, 4065992.932112665, 4063162.143518177, 4060798.1858924176, 3956764.654354398, 3912916.1668887464, 4018282.0763658765, 4065575.3280486814, 3967348.3916016137, 4034992.477051428, 4069123.2018048204, 3939281.4172981237, 4022103.802712647, 4083993.320300048, 4034478.871034405, 4068844.513451607, 4097187.535489012, 3981130.4047553614, 4068312.6406908804, 4050921.0879167155, 4048297.277514315, 3953878.475004285, 3998627.3710734197 ], "12": [ 4007152.5182738686, 4014664.8542149696, 4095619.5802802853, 4018084.7270321106, 4072050.3744347296, 4026256.723716898, 4095827.9573665825, 4023631.9896559394, 4046751.9125588783, 3973758.674124694, 4081927.075527175, 3922485.387310559, 4001549.2805312183, 4050417.849670596, 3987607.4531957353, 4060206.9664999805, 4080316.8473846694, 4030455.1532406537, 4087714.965906726, 4028165.0792610054, 4032588.5261474997, 3980546.468460318, 4090408.033691761, 3990019.103297975, 4088755.998466496, 4092162.22327816, 4029036.6583707742, 4055066.505591603, 4081998.821392285, 4079550.553314541 ], "13": [ 3905319.849889843, 4054719.0660902266, 4055596.4319745116, 3992648.989962779, 3924972.5941170114, 4095167.7814041013, 3912740.1944122575, 4024882.9438952096, 4023171.3988155797, 4059892.954049364, 4068510.96886605, 4093838.431690223, 4070524.1327491063 ], "14": [ 4092261.8249403643, 3956304.3865069468, 4069053.2302732924, 4038890.8473817194 ], "15": [ 4013891.110502415, 3977489.9532032954, 4044335.989753631, 4066199.8081775964 ], "16": [ 3979706.1687804307, 4024156.037977316 ], "17": [] } \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json deleted file mode 100644 index a645c5b..0000000 --- a/backend/tsconfig.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } -} diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs deleted file mode 100644 index d6c9537..0000000 --- a/frontend/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -} diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 0d6babe..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index e4b78ea..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 917abf8..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,4362 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.0", - "dependencies": { - "axios": "^1.7.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-icons": "^5.2.1", - "react-router-dom": "^6.23.1" - }, - "devDependencies": { - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.3", - "typescript": "^5.2.2", - "vite": "^5.2.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.24.2", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz", - "integrity": "sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz", - "integrity": "sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@remix-run/router": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", - "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", - "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.3.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz", - "integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==", - "dev": true, - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz", - "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/type-utils": "7.9.0", - "@typescript-eslint/utils": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz", - "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/typescript-estree": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz", - "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz", - "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "7.9.0", - "@typescript-eslint/utils": "7.9.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", - "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", - "dev": true, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz", - "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz", - "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/typescript-estree": "7.9.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz", - "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.9.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", - "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.5", - "@babel/plugin-transform-react-jsx-self": "^7.23.3", - "@babel/plugin-transform-react-jsx-source": "^7.23.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" - } - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axios": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.1.tgz", - "integrity": "sha512-+LV37nQcd1EpFalkXksWNBiA17NZ5m5/WspmHGmZmdx1qBOg/VNq/c4eRJiA9VQQHBOs+N0ZhhdU10h2TyNK7Q==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001620", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", - "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.4.774", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.774.tgz", - "integrity": "sha512-132O1XCd7zcTkzS3FgkAzKmnBuNJjK8WjcTtNuoylj7MYbqw5eXehjQ5OK91g0zm7OTKIPeaAG4CPoRfD9M1Mg==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", - "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", - "dev": true, - "peerDependencies": { - "eslint": ">=7" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "dev": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", - "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", - "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-icons": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", - "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", - "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", - "dependencies": { - "@remix-run/router": "1.16.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", - "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", - "dependencies": { - "@remix-run/router": "1.16.1", - "react-router": "6.23.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", - "dev": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", - "dev": true, - "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", - "dev": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 71b21d4..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" - }, - "dependencies": { - "axios": "^1.7.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-icons": "^5.2.1", - "react-router-dom": "^6.23.1" - }, - "devDependencies": { - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.3", - "typescript": "^5.2.2", - "vite": "^5.2.0" - } -} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index bf170c9..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// import "./App.css"; -import { BrowserRouter, Routes, Route } from "react-router-dom"; -import { Simulation } from "./pages/Simulation"; -import { Game } from "./pages/Game"; -import { Footer, Navbar } from "./components"; -import { Home } from "./pages/Home"; - -function App() { - return ( - - - - } /> - } /> - } /> - -