Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# dependencies
node_modules

# build outputs
.turbo
dist
.next
coverage
*.tsbuildinfo

# environment
.env
.env.*
!.env.example

# OS / editor
.DS_Store
*.log
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
# MatchPoint

MatchPoint is a Turborepo + pnpm monorepo scaffold for an esports platform.

## Workspace layout

- `apps/web` — Next.js TypeScript frontend
- `apps/api` — Express TypeScript backend
- `packages/db` — Prisma schema and client package

## Quick start

```bash
corepack enable
pnpm install
pnpm dev
```

## Infrastructure

- Local PostgreSQL is available via `docker-compose.yml`.
- Prisma uses `DATABASE_URL` from environment variables.
22 changes: 22 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@matchpoint/api",
"version": "0.0.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"express": "^5.1.0"
},
"devDependencies": {
"@types/express": "^5.0.1",
"@types/node": "^22.15.21",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
}
}
12 changes: 12 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import express from 'express';

const app = express();
const port = Number(process.env.PORT ?? 4000);

app.get('/health', (_req, res) => {
res.status(200).json({ status: 'ok', service: 'api' });
});

app.listen(port, () => {
console.log(`@matchpoint/api listening on port ${port}`);
});
16 changes: 16 additions & 0 deletions apps/api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}
19 changes: 19 additions & 0 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from 'next';
import type { ReactNode } from 'react';

export const metadata: Metadata = {
title: 'MatchPoint',
description: 'MatchPoint esports platform',
};

type RootLayoutProps = {
children: ReactNode;
};

export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
3 changes: 3 additions & 0 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function HomePage() {
return <main>MatchPoint web scaffold is ready.</main>;
}
19 changes: 19 additions & 0 deletions apps/web/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { FlatCompat } from '@eslint/eslintrc';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
});

const eslintConfig = [
{
ignores: ['.next/**', 'node_modules/**', 'next-env.d.ts'],
},
...compat.extends('next/core-web-vitals', 'next/typescript'),
];

export default eslintConfig;
6 changes: 6 additions & 0 deletions apps/web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 changes: 6 additions & 0 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};

module.exports = nextConfig;
26 changes: 26 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@matchpoint/web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --port 3000",
"build": "next build",
"start": "next start --port 3000",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "^15.3.2",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@types/node": "^22.15.21",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"eslint": "^9.26.0",
"eslint-config-next": "^15.3.2",
"typescript": "^5.8.3"
}
}
20 changes: 20 additions & 0 deletions apps/web/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "es2022"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: matchpoint
POSTGRES_PASSWORD: matchpoint
POSTGRES_DB: matchpoint
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data

volumes:
postgres_data:
17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "matchpoint",
"private": true,
"packageManager": "pnpm@10.11.0",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"format": "prettier --check .",
"format:write": "prettier --write ."
},
"devDependencies": {
"prettier": "^3.5.3",
"turbo": "^2.5.4"
}
}
17 changes: 17 additions & 0 deletions packages/db/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@matchpoint/db",
"version": "0.0.0",
"private": true,
"scripts": {
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:push": "prisma db push",
"prisma:studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^6.8.2"
},
"devDependencies": {
"prisma": "^6.8.2"
}
}
121 changes: 121 additions & 0 deletions packages/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

enum GameTitle {
RAINBOW_SIX_SIEGE
}

enum Platform {
PC
PLAYSTATION
XBOX
}

enum Region {
NA
EU
APAC
LATAM
MENA
}

enum MatchStatus {
SCHEDULED
IN_PROGRESS
COMPLETED
CANCELED
}

enum TeamSide {
ATTACK
DEFENSE
}

enum MatchResult {
WIN
LOSS
}

model User {
id String @id @default(cuid())
email String @unique
displayName String
platform Platform
region Region
gameTitle GameTitle @default(RAINBOW_SIX_SIEGE)
rank String?
teamMemberships TeamMember[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Team {
id String @id @default(cuid())
name String @unique
gameTitle GameTitle @default(RAINBOW_SIX_SIEGE)
region Region
members TeamMember[]
homeMatches Match[] @relation("HomeTeamMatches")
awayMatches Match[] @relation("AwayTeamMatches")
participants MatchParticipant[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model TeamMember {
id String @id @default(cuid())
teamId String
userId String
isCaptain Boolean @default(false)
joinedAt DateTime @default(now())
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([teamId, userId])
}

model SiegeMap {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
matches Match[]
}

model Match {
id String @id @default(cuid())
gameTitle GameTitle @default(RAINBOW_SIX_SIEGE)
scheduledAt DateTime
status MatchStatus @default(SCHEDULED)
homeTeamId String
awayTeamId String
mapId String?
bestOf Int @default(1)
winnerTeamId String?
homeTeam Team @relation("HomeTeamMatches", fields: [homeTeamId], references: [id], onDelete: Restrict)
awayTeam Team @relation("AwayTeamMatches", fields: [awayTeamId], references: [id], onDelete: Restrict)
map SiegeMap? @relation(fields: [mapId], references: [id], onDelete: SetNull)
participants MatchParticipant[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([scheduledAt])
}

model MatchParticipant {
id String @id @default(cuid())
matchId String
teamId String
side TeamSide
result MatchResult?
score Int?
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamId], references: [id], onDelete: Restrict)

@@unique([matchId, teamId])
}
Loading