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
72 changes: 72 additions & 0 deletions .agents/skills/conventional-commit/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
<description>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.</description>
```

### 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 <file>`.
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
<commit-message>
<type>feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert</type>
<scope>()</scope>
<description>A short, imperative summary of the change</description>
<body>(optional: more detailed explanation)</body>
<footer>(optional: e.g. BREAKING CHANGE: details, or issue references)</footer>
</commit-message>
```

### Examples

```xml
<examples>
<example>feat(parser): add ability to parse arrays</example>
<example>fix(ui): correct button alignment</example>
<example>docs: update README with usage instructions</example>
<example>refactor: improve performance of data processing</example>
<example>chore: update dependencies</example>
<example>feat!: send email on registration (BREAKING CHANGE: email service required)</example>
</examples>
```

### Validation

```xml
<validation>
<type>Must be one of the allowed types. See <reference>https://www.conventionalcommits.org/en/v1.0.0/#specification</reference></type>
<scope>Optional, but recommended for clarity.</scope>
<description>Required. Use the imperative mood (e.g., "add", not "added").</description>
<body>Optional. Use for additional context.</body>
<footer>Use for breaking changes or issue references.</footer>
</validation>
```

### Final Step

```xml
<final-step>
<cmd>git commit -m "type(scope): description"</cmd>
<note>Replace with your constructed message. Include body and footer if needed.</note>
</final-step>
```
1 change: 1 addition & 0 deletions .claude/skills/conventional-commit
194 changes: 144 additions & 50 deletions app/[locale]/admin/events/[id]/stages/[stageId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
UserPlus,
UserX,
Heart,
RefreshCw,
Eye,
EyeOff,
} from 'lucide-react';
import { toast } from 'sonner';

Expand Down Expand Up @@ -52,6 +55,7 @@ import {
useParticipantControllerRemovePartner,
getParticipantControllerFindByStageQueryKey,
} from '@/lib/services/participants/participants';
import { useMatchScoreControllerFindAll } from '@/lib/services/match-scores/match-scores';

const STAGE_TYPE_LABELS: Record<string, string> = {
ROUND_ROBIN_PLAYOFF: 'Round Robin + Playoff',
Expand Down Expand Up @@ -81,6 +85,132 @@ const MATCH_STATUS_CONFIG: Record<string, { className: string; label: string }>
CANCELLED: { className: 'bg-gray-500 text-white', label: 'Cancelled' },
};

function MatchRow({ match, statusConfig }: { match: any; statusConfig: Record<string, { className: string; label: string }> }) {
const [showScore, setShowScore] = useState(false);

const { data: scoresData, isFetching: isFetchingScores, refetch } = useMatchScoreControllerFindAll(
match.id,
{ query: { enabled: false } },
);

const handleShowScore = () => {
setShowScore(true);
refetch();
};

const handleHideScore = () => {
setShowScore(false);
};

const scores: any[] = Array.isArray(scoresData) ? scoresData : (scoresData as any)?.data ?? [];

return (
<div
className={`flex items-center justify-between rounded-md border p-3 text-sm ${match.status === 'IN_PROGRESS' ? 'bg-red-50 border-red-300' : match.status === 'COMPLETED' ? 'bg-green-50 border-green-300' : match.status === 'CANCELLED' ? 'bg-gray-100 border-gray-300' : ''}`}
>
<div className="flex items-center gap-4">
<span className="text-xs text-muted-foreground w-6 text-right font-mono">
#{match.matchNumber || '-'}
</span>
<div className="min-w-0">
<p className="font-medium truncate">{match.name}</p>
{(match.startTime || match.endTime) && (
<p className="text-xs text-muted-foreground mt-0.5">
{match.startTime && (
<span>Start: {new Date(match.startTime).toLocaleString()}</span>
)}
{match.startTime && match.endTime && <span className="mx-1">·</span>}
{match.endTime && (
<span>End: {new Date(match.endTime).toLocaleString()}</span>
)}
</p>
)}
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
{match.isBye ? (
<span className="italic">BYE</span>
) : (
<>
<span className={match.winnerTeam === 1 ? 'font-semibold text-foreground' : ''}>
{match.team1Name ||
[match.team1Player1?.name, match.team1Player2?.name].filter(Boolean).join(' / ') ||
'TBD'}
</span>
<span>vs</span>
<span className={match.winnerTeam === 2 ? 'font-semibold text-foreground' : ''}>
{match.team2Name ||
[match.team2Player1?.name, match.team2Player2?.name].filter(Boolean).join(' / ') ||
'TBD'}
</span>
</>
)}
</div>
{showScore && (
<div className="mt-1.5">
{isFetchingScores ? (
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
) : scores.length > 0 ? (
<div className="flex flex-col gap-1 font-mono text-xs">
{scores
.sort((a: any, b: any) => a.setNumber - b.setNumber)
.map((score: any) => (
<span
key={score.id}
className={`px-1.5 py-0.5 rounded ${
score.winnerTeam === 1
? 'bg-green-100 text-green-800'
: score.winnerTeam === 2
? 'bg-red-100 text-red-800'
: 'bg-gray-100 text-gray-600'
}`}
>
Set {score.setNumber}: {score.team1Points}-{score.team2Points}
</span>
))}
</div>
) : (
<span className="text-xs text-muted-foreground italic">No scores yet</span>
)}
</div>
)}
</div>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
className="h-7 px-2 text-xs"
onClick={showScore ? handleHideScore : handleShowScore}
disabled={isFetchingScores}
>
{isFetchingScores ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : showScore ? (
<>
<EyeOff className="h-3 w-3 mr-1" />
Hide Score
</>
) : (
<>
<Eye className="h-3 w-3 mr-1" />
Show Score
</>
)}
</Button>
{match.bracketType && (
<Badge variant="outline" className="text-xs">
{match.bracketType}
</Badge>
)}
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${statusConfig[match.status]?.className || 'bg-gray-100 text-gray-600'}`}
>
{statusConfig[match.status]?.label || match.status}
</span>
</div>
</div>
);
}

export default function StageDetailPage() {
const params = useParams();
const router = useRouter();
Expand All @@ -91,14 +221,15 @@ export default function StageDetailPage() {
const [activeTab, setActiveTab] = useState('matches');
const [assigningParticipant, setAssigningParticipant] = useState<any | null>(null);
const [selectedPartnerId, setSelectedPartnerId] = useState('');
const [reloadCount, setReloadCount] = useState(0);

const { data: stageData, isLoading } = useStageControllerFindOne(eventId, stageId, {
query: { enabled: !!eventId && !!stageId },
});
const stage = stageData as any;

// Correct: use stageId directly for matches
const { data: matchesData } = useStageControllerFindMatchesByStage(eventId, stageId, {
const { data: matchesData, refetch: refetchMatches, isFetching: isFetchingMatches } = useStageControllerFindMatchesByStage(eventId, stageId, {
query: { enabled: !!eventId && !!stageId },
});

Expand Down Expand Up @@ -314,6 +445,7 @@ export default function StageDetailPage() {

{/* Tabs: Matches / Participants */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<div className="flex items-center justify-between">
<TabsList>
<TabsTrigger value="matches" className="flex items-center gap-1.5">
<Swords className="h-4 w-4" />
Expand All @@ -334,6 +466,16 @@ export default function StageDetailPage() {
)}
</TabsTrigger>
</TabsList>
<Button
variant="outline"
size="sm"
onClick={() => { refetchMatches(); setReloadCount(c => c + 1); }}
disabled={isFetchingMatches}
>
<RefreshCw className={`h-4 w-4 mr-1.5 ${isFetchingMatches ? 'animate-spin' : ''}`} />
Reload
</Button>
</div>

{/* Matches Tab */}
<TabsContent value="matches" className="mt-4">
Expand Down Expand Up @@ -368,55 +510,7 @@ export default function StageDetailPage() {
{roundMatches
.sort((a: any, b: any) => (a.matchNumber || 0) - (b.matchNumber || 0))
.map((match: any) => (
<div
key={match.id}
className={`flex items-center justify-between rounded-md border p-3 text-sm ${match.status === 'IN_PROGRESS' ? 'bg-red-50 border-red-300' : match.status === 'COMPLETED' ? 'bg-green-50 border-green-300' : match.status === 'CANCELLED' ? 'bg-gray-100 border-gray-300' : ''}`}
>
<div className="flex items-center gap-4">
<span className="text-xs text-muted-foreground w-6 text-right font-mono">
#{match.matchNumber || '-'}
</span>
<div className="min-w-0">
<p className="font-medium truncate">{match.name}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
{match.isBye ? (
<span className="italic">BYE</span>
) : (
<>
<span className={match.winnerTeam === 1 ? 'font-semibold text-foreground' : ''}>
{match.team1Name ||
[match.team1Player1?.name, match.team1Player2?.name].filter(Boolean).join(' / ') ||
'TBD'}
</span>
<span>vs</span>
<span className={match.winnerTeam === 2 ? 'font-semibold text-foreground' : ''}>
{match.team2Name ||
[match.team2Player1?.name, match.team2Player2?.name].filter(Boolean).join(' / ') ||
'TBD'}
</span>
</>
)}
</div>
</div>
</div>
<div className="flex items-center gap-3">
{match.team1Score && match.team2Score && (
<span className="font-mono text-sm">
{match.team1Score.total ?? '-'} : {match.team2Score.total ?? '-'}
</span>
)}
{match.bracketType && (
<Badge variant="outline" className="text-xs">
{match.bracketType}
</Badge>
)}
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${MATCH_STATUS_CONFIG[match.status]?.className || 'bg-gray-100 text-gray-600'}`}
>
{MATCH_STATUS_CONFIG[match.status]?.label || match.status}
</span>
</div>
</div>
<MatchRow key={`${match.id}-${reloadCount}`} match={match} statusConfig={MATCH_STATUS_CONFIG} />
))}
</div>
</CardContent>
Expand Down
13 changes: 13 additions & 0 deletions lib/schemas/athleteEmailDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Generated by orval v8.1.0 🍺
* Do not edit manually.
* 5Sport API
* API documentation for 5Sport authentication and services
* OpenAPI spec version: 1.0
*/

export interface AthleteEmailDto {
name: string;
distance: string;
email: string;
}
2 changes: 2 additions & 0 deletions lib/schemas/campaignPublicResponseDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface CampaignPublicResponseDto {
hotline?: string;
/** Link điều lệ */
regulationsUrl?: string;
/** Nội dung thể lệ (Markdown) */
regulations?: string;
/** Link fanpage */
fanpageUrl?: string;
/** Danh sách size áo */
Expand Down
2 changes: 2 additions & 0 deletions lib/schemas/createCampaignDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface CreateCampaignDto {
hotline?: string;
/** Link điều lệ giải */
regulationsUrl?: string;
/** Nội dung thể lệ (Markdown) */
regulations?: string;
/** Link fanpage */
fanpageUrl?: string;
/** Danh sách size áo của campaign */
Expand Down
4 changes: 4 additions & 0 deletions lib/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './athleteControllerFindAllSortOrder';
export * from './athleteControllerFindAllSportType';
export * from './athleteControllerGetStatsParams';
export * from './athleteControllerSearchParams';
export * from './athleteEmailDto';
export * from './athleteInfoDto';
export * from './athleteInfoDtoGender';
export * from './athleteInfoDtoSizeShirt';
Expand Down Expand Up @@ -170,6 +171,7 @@ export * from './stageConfig';
export * from './stageStageType';
export * from './stageStatus';
export * from './swapCourtDto';
export * from './testGroupOrderEmailDto';
export * from './ticketTierResponseDto';
export * from './unassignMatchDto';
export * from './updateAthleteDto';
Expand Down Expand Up @@ -203,6 +205,8 @@ export * from './updateStageDtoConfig';
export * from './updateStageDtoStageType';
export * from './updateTicketTierDto';
export * from './uploadControllerUploadFileBody';
export * from './upsertMatchScoreDto';
export * from './upsertMatchScoreDtoDetails';
export * from './user';
export * from './userResponseDto';
export * from './userResponseDtoRole';
Expand Down
Loading
Loading