diff --git a/.agents/skills/conventional-commit/SKILL.md b/.agents/skills/conventional-commit/SKILL.md new file mode 100644 index 0000000..3884217 --- /dev/null +++ b/.agents/skills/conventional-commit/SKILL.md @@ -0,0 +1,72 @@ +--- +name: conventional-commit +description: 'Prompt and workflow for generating conventional commit messages using a structured XML format. Guides users to create standardized, descriptive commit messages in line with the Conventional Commits specification, including instructions, examples, and validation.' +--- + +### Instructions + +```xml + This file contains a prompt template for generating conventional commit messages. It provides instructions, examples, and formatting guidelines to help users write standardized, descriptive commit messages in accordance with the Conventional Commits specification. +``` + +### Workflow + +**Follow these steps:** + +1. Run `git status` to review changed files. +2. Run `git diff` or `git diff --cached` to inspect changes. +3. Stage your changes with `git add `. +4. Construct your commit message using the following XML structure. +5. After generating your commit message, Copilot will automatically run the following command in your integrated terminal (no confirmation needed): + +```bash +git commit -m "type(scope): description" +``` + +6. Just execute this prompt and Copilot will handle the commit for you in the terminal. + +### Commit Message Structure + +```xml + + feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert + () + A short, imperative summary of the change + (optional: more detailed explanation) +
(optional: e.g. BREAKING CHANGE: details, or issue references)
+
+``` + +### Examples + +```xml + + feat(parser): add ability to parse arrays + fix(ui): correct button alignment + docs: update README with usage instructions + refactor: improve performance of data processing + chore: update dependencies + feat!: send email on registration (BREAKING CHANGE: email service required) + +``` + +### Validation + +```xml + + Must be one of the allowed types. See https://www.conventionalcommits.org/en/v1.0.0/#specification + Optional, but recommended for clarity. + Required. Use the imperative mood (e.g., "add", not "added"). + Optional. Use for additional context. +
Use for breaking changes or issue references.
+
+``` + +### Final Step + +```xml + + git commit -m "type(scope): description" + Replace with your constructed message. Include body and footer if needed. + +``` diff --git a/.claude/skills/conventional-commit b/.claude/skills/conventional-commit new file mode 120000 index 0000000..6479df2 --- /dev/null +++ b/.claude/skills/conventional-commit @@ -0,0 +1 @@ +../../.agents/skills/conventional-commit \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..2aca4c4 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "skills": { + "conventional-commit": { + "source": "github/awesome-copilot", + "sourceType": "github", + "computedHash": "f29c9486cede6c7b2df0cfb0a2e4aa67446552b991bcf17d1b309e903171f03d" + } + } +} diff --git a/src/modules/event/dto/match/index.ts b/src/modules/event/dto/match/index.ts index 798e27f..985236d 100644 --- a/src/modules/event/dto/match/index.ts +++ b/src/modules/event/dto/match/index.ts @@ -1,3 +1,4 @@ export * from './create-match.dto'; export * from './update-match.dto'; export * from './update-score.dto'; +export * from './upsert-match-score.dto'; diff --git a/src/modules/event/dto/match/upsert-match-score.dto.ts b/src/modules/event/dto/match/upsert-match-score.dto.ts new file mode 100644 index 0000000..4673842 --- /dev/null +++ b/src/modules/event/dto/match/upsert-match-score.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNumber, IsNotEmpty, IsOptional, IsObject, Min } from 'class-validator'; + +export class UpsertMatchScoreDto { + @ApiProperty({ description: 'Team 1 points in this set', example: 21 }) + @IsNumber() + @IsNotEmpty() + @Min(0) + team1Points: number; + + @ApiProperty({ description: 'Team 2 points in this set', example: 19 }) + @IsNumber() + @IsNotEmpty() + @Min(0) + team2Points: number; + + @ApiPropertyOptional({ description: 'Winner team for this set (1 or 2)', example: 1 }) + @IsNumber() + @IsOptional() + winnerTeam?: number; + + @ApiPropertyOptional({ + description: 'Point-by-point details', + example: { points: [{ team: 1, score: [1, 0] }] }, + }) + @IsObject() + @IsOptional() + details?: Record; +} diff --git a/src/modules/event/event.module.ts b/src/modules/event/event.module.ts index 9d5d194..22ae441 100644 --- a/src/modules/event/event.module.ts +++ b/src/modules/event/event.module.ts @@ -17,6 +17,7 @@ import { Stage } from './entities/stage.entity'; import { EventController } from './event.controller'; import { PublicEventController } from './public-event.controller'; import { MatchController } from './match.controller'; +import { MatchScoreController } from './match-score.controller'; import { ParticipantController } from './participant.controller'; import { StageController } from './stage.controller'; @@ -51,6 +52,7 @@ import { FlexStrategy } from './strategies/flex.strategy'; EventController, PublicEventController, MatchController, + MatchScoreController, ParticipantController, StageController, ], diff --git a/src/modules/event/match-score.controller.ts b/src/modules/event/match-score.controller.ts new file mode 100644 index 0000000..43f130e --- /dev/null +++ b/src/modules/event/match-score.controller.ts @@ -0,0 +1,46 @@ +import { Body, Controller, Get, Param, ParseIntPipe, ParseUUIDPipe, Put } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { MatchService } from './match.service'; +import { UpsertMatchScoreDto } from './dto/match'; +import { MatchScore } from './entities'; + +@ApiTags('match-scores') +@Controller('matches/:matchId/scores') +export class MatchScoreController { + constructor(private readonly matchService: MatchService) {} + + @Get() + @ApiOperation({ summary: 'Get all scores of a match' }) + @ApiParam({ name: 'matchId', description: 'Match ID' }) + @ApiResponse({ + status: 200, + description: 'Returns all scores for the match ordered by set number', + type: [MatchScore], + }) + async findAll( + @Param('matchId', ParseUUIDPipe) matchId: string, + ): Promise { + return await this.matchService.getScores(matchId); + } + + @Put(':setNumber') + @ApiOperation({ + summary: 'Create or update a match score', + description: + 'Creates or updates the score for a specific set. Match must be IN_PROGRESS. ' + + 'Set N can only be created after set N-1 exists.', + }) + @ApiParam({ name: 'matchId', description: 'Match ID' }) + @ApiParam({ name: 'setNumber', description: 'Set number (1-based)', example: 1 }) + @ApiBody({ type: UpsertMatchScoreDto }) + @ApiResponse({ status: 200, description: 'Score created or updated', type: MatchScore }) + @ApiResponse({ status: 400, description: 'Match not IN_PROGRESS or previous set missing' }) + @ApiResponse({ status: 404, description: 'Match not found' }) + async upsert( + @Param('matchId', ParseUUIDPipe) matchId: string, + @Param('setNumber', ParseIntPipe) setNumber: number, + @Body() dto: UpsertMatchScoreDto, + ): Promise { + return await this.matchService.upsertScore(matchId, setNumber, dto); + } +} diff --git a/src/modules/event/match.service.ts b/src/modules/event/match.service.ts index e846e0d..664145e 100644 --- a/src/modules/event/match.service.ts +++ b/src/modules/event/match.service.ts @@ -2,7 +2,7 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/comm import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Match, MatchScore, EventParticipant } from './entities'; -import { CreateMatchDto, UpdateMatchDto, UpdateScoreDto } from './dto/match'; +import { CreateMatchDto, UpdateMatchDto, UpdateScoreDto, UpsertMatchScoreDto } from './dto/match'; import { MatchStatus } from './entities/match.entity'; @Injectable() @@ -144,6 +144,40 @@ export class MatchService { await this.matchRepository.save(match); } + async upsertScore(matchId: string, setNumber: number, dto: UpsertMatchScoreDto): Promise { + const match = await this.matchRepository.findOne({ where: { id: matchId } }); + if (!match) { + throw new NotFoundException(`Match ${matchId} not found`); + } + if (match.status !== MatchStatus.IN_PROGRESS) { + throw new BadRequestException('Match must be IN_PROGRESS to modify scores'); + } + + const existing = await this.scoreRepository.findOne({ where: { matchId, setNumber } }); + + if (!existing && setNumber > 1) { + const prevSet = await this.scoreRepository.findOne({ + where: { matchId, setNumber: setNumber - 1 }, + }); + if (!prevSet) { + throw new BadRequestException( + `Set ${setNumber - 1} must exist before creating set ${setNumber}`, + ); + } + } + + let score = existing; + if (score) { + Object.assign(score, dto); + } else { + score = this.scoreRepository.create({ matchId, setNumber, ...dto }); + } + + const savedScore = await this.scoreRepository.save(score); + await this.updateMatchAggregateScores(matchId); + return savedScore; + } + async getScores(matchId: string): Promise { return await this.scoreRepository.find({ where: { matchId },