diff --git a/.github/workflows/api-deploy.yml b/.github/workflows/api-deploy.yml index d92b73ed..8e0cb5f3 100644 --- a/.github/workflows/api-deploy.yml +++ b/.github/workflows/api-deploy.yml @@ -19,8 +19,8 @@ on: - prod concurrency: - group: ${{ format('{0}-{1}-{2}', github.workflow, github.ref, github.event_name == 'workflow_call' && github.run_id || 'manual') }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment }} + cancel-in-progress: false env: REGISTRY: ghcr.io diff --git a/.github/workflows/frontend-deploy.yml b/.github/workflows/frontend-deploy.yml index c146bbec..c283a3c8 100644 --- a/.github/workflows/frontend-deploy.yml +++ b/.github/workflows/frontend-deploy.yml @@ -19,8 +19,8 @@ on: - prod concurrency: - group: ${{ format('{0}-{1}-{2}', github.workflow, github.ref, github.event_name == 'workflow_call' && github.run_id || 'manual') }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment }} + cancel-in-progress: false env: REGISTRY: ghcr.io diff --git a/.github/workflows/github-service-deploy.yml b/.github/workflows/github-service-deploy.yml index 13695d5b..237b9819 100644 --- a/.github/workflows/github-service-deploy.yml +++ b/.github/workflows/github-service-deploy.yml @@ -19,8 +19,8 @@ on: - prod concurrency: - group: ${{ format('{0}-{1}-{2}', github.workflow, github.ref, github.event_name == 'workflow_call' && github.run_id || 'manual') }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment }} + cancel-in-progress: false env: REGISTRY: ghcr.io diff --git a/.github/workflows/k8s-service-deploy.yml b/.github/workflows/k8s-service-deploy.yml index 3702e1cc..066b3c45 100644 --- a/.github/workflows/k8s-service-deploy.yml +++ b/.github/workflows/k8s-service-deploy.yml @@ -19,8 +19,8 @@ on: - prod concurrency: - group: ${{ format('{0}-{1}-{2}', github.workflow, github.ref, github.event_name == 'workflow_call' && github.run_id || 'manual') }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment }} + cancel-in-progress: false env: REGISTRY: ghcr.io diff --git a/api/src/match/match.controller.ts b/api/src/match/match.controller.ts index fe40c76a..f6616d68 100644 --- a/api/src/match/match.controller.ts +++ b/api/src/match/match.controller.ts @@ -21,7 +21,7 @@ export class MatchController { constructor( private readonly matchService: MatchService, private readonly eventService: EventService, - ) { } + ) {} private logger = new Logger("MatchController"); @@ -113,9 +113,7 @@ export class MatchController { @UseGuards(JwtAuthGuard) @Get("team/:teamId") - async getMatchesForTeam( - @Param("teamId", ParseUUIDPipe) teamId: string, - ) { + async getMatchesForTeam(@Param("teamId", ParseUUIDPipe) teamId: string) { return await this.matchService.getMatchesForTeam(teamId); } @@ -156,6 +154,21 @@ export class MatchController { return this.matchService.revealMatch(matchId); } + @UseGuards(JwtAuthGuard) + @Put("reveal-all/:eventId/:phase") + async revealAllMatches( + @Param("eventId", ParseUUIDPipe) eventId: string, + @Param("phase") phase: string, + @UserId() userId: string, + ) { + if (!(await this.eventService.isEventAdmin(eventId, userId))) + throw new UnauthorizedException( + "You are not authorized to reveal matches for this event.", + ); + + return this.matchService.revealAllMatchesInPhase(eventId, phase as any); + } + @Get(":matchId") async getMatchById( @Param("matchId", ParseUUIDPipe) matchId: string, diff --git a/api/src/match/match.service.ts b/api/src/match/match.service.ts index a205c31f..9a3ceab2 100644 --- a/api/src/match/match.service.ts +++ b/api/src/match/match.service.ts @@ -282,10 +282,7 @@ export class MatchService { } const tournamentTeamCount = await this.getTournamentTeamCount(event.id); - const finalRoundIndex = Math.max( - 0, - Math.log2(tournamentTeamCount) - 1, - ); + const finalRoundIndex = Math.max(0, Math.log2(tournamentTeamCount) - 1); if (event.currentRound >= finalRoundIndex) { this.logger.log(`Event ${event.name} has finished the final round.`); @@ -833,18 +830,23 @@ export class MatchService { select: { id: true, }, - where: { - teams: { - id: teamId, + where: [ + { + teams: { id: teamId }, + state: MatchState.FINISHED, + phase: MatchPhase.QUEUE, }, - state: MatchState.FINISHED, - }, + { + teams: { id: teamId }, + state: MatchState.FINISHED, + isRevealed: true, + }, + ], withDeleted: true, }) - ).map(match => match.id); + ).map((match) => match.id); - if (matchesToQuery.length === 0) - return []; + if (matchesToQuery.length === 0) return []; const matches = await this.matchRepository.find({ where: { @@ -864,8 +866,15 @@ export class MatchService { }); return matches.map((match) => { - const { id: _id, ...rest } = match; - return rest; + // Reveal ID only if it's NOT from the queue AND it is revealed. + const shouldRevealId = + match.phase !== MatchPhase.QUEUE && match.isRevealed; + + if (!shouldRevealId) { + const { id: _id, ...rest } = match; + return rest; + } + return match; }); } @@ -1025,6 +1034,26 @@ export class MatchService { }); } + async revealAllMatchesInPhase(eventId: string, phase: MatchPhase) { + const matches = await this.matchRepository.find({ + where: { + teams: { + event: { + id: eventId, + }, + }, + phase: phase, + }, + }); + + if (matches.length > 0) { + await this.matchRepository.update( + matches.map((m) => m.id), + { isRevealed: true }, + ); + } + } + getGlobalStats() { return this.matchStatsRepository .createQueryBuilder("match_stats") diff --git a/api/src/team/team.controller.ts b/api/src/team/team.controller.ts index f9f1e48d..ff7ef50f 100644 --- a/api/src/team/team.controller.ts +++ b/api/src/team/team.controller.ts @@ -134,7 +134,13 @@ export class TeamController { @EventId eventId: string, @Body() inviteUserDto: InviteUserDto, @Team() team: TeamEntity, + @UserId() userId: string ) { + if(userId === inviteUserDto.userToInviteId) + throw new BadRequestException( + "You cannot invite yourself to a team.", + ) + if(await this.teamService.isTeamFull(team.id)) throw new BadRequestException("This team is full."); if ( @@ -169,8 +175,9 @@ export class TeamController { @EventId eventId: string, @Param("searchQuery") searchQuery: string, @Team() team: TeamEntity, + @UserId() userId: string ) { - return this.userService.searchUsersForInvite(eventId, searchQuery, team.id); + return this.userService.searchUsersForInvite(eventId, searchQuery, team.id, userId); } @UseGuards(JwtAuthGuard) @@ -195,6 +202,7 @@ export class TeamController { ); if (!(await this.teamService.isUserInvitedToTeam(userId, teamId))) throw new BadRequestException("You are not invited to this team."); + if(await this.teamService.isTeamFull(teamId)) throw new BadRequestException("This team is full."); diff --git a/api/src/team/team.service.ts b/api/src/team/team.service.ts index 50f22cac..4e3b7366 100644 --- a/api/src/team/team.service.ts +++ b/api/src/team/team.service.ts @@ -15,7 +15,6 @@ import { FindOptionsRelations } from "typeorm/find-options/FindOptionsRelations" import { MatchService } from "../match/match.service"; import { Cron, CronExpression } from "@nestjs/schedule"; import { LockKeys } from "../constants"; -import { EventEntity } from "../event/entities/event.entity"; @Injectable() export class TeamService { diff --git a/api/src/user/user.service.ts b/api/src/user/user.service.ts index fe47687d..7b1f47ed 100644 --- a/api/src/user/user.service.ts +++ b/api/src/user/user.service.ts @@ -12,8 +12,8 @@ export class UserService { constructor( @InjectRepository(UserEntity) private readonly userRepository: Repository, - private configService: ConfigService, - ) { } + private readonly configService: ConfigService, + ) {} async createUser( email: string, @@ -128,6 +128,7 @@ export class UserService { eventId: string, searchQuery: string, teamId: string, + userId: string, ): Promise { const isEventPublic = await this.userRepository.manager .getRepository(EventEntity) @@ -159,6 +160,7 @@ export class UserService { social: `%${searchQuery}%`, }, ) + .andWhere("user.id != :userId", { userId }) .andWhere("team.id IS NULL") .andWhere( "(inviteEvent.id IS NULL OR inviteEvent.id != :eventId OR inviteTeam.id = :teamId)", diff --git a/frontend/app/actions/tournament-model.ts b/frontend/app/actions/tournament-model.ts index 672d19a1..00115749 100644 --- a/frontend/app/actions/tournament-model.ts +++ b/frontend/app/actions/tournament-model.ts @@ -11,7 +11,7 @@ export enum MatchState { } export interface Match { - id: string; + id?: string; round: number; state: MatchState; phase: MatchPhase; diff --git a/frontend/app/actions/tournament.ts b/frontend/app/actions/tournament.ts index 44e62d11..d6d8327c 100644 --- a/frontend/app/actions/tournament.ts +++ b/frontend/app/actions/tournament.ts @@ -51,15 +51,22 @@ export async function revealMatch( return handleError(axiosInstance.put(`/match/reveal/${matchId}`)); } +export async function revealAllMatches( + eventId: string, + phase: string, +): Promise> { + return handleError( + axiosInstance.put(`/match/reveal-all/${eventId}/${phase}`), + ); +} + export async function getMatchById( matchId: string, ): Promise> { return handleError(axiosInstance.get(`/match/${matchId}`)); } -export async function getMatchesForTeam( - teamId: string, -): Promise { +export async function getMatchesForTeam(teamId: string): Promise { return (await axiosInstance.get(`/match/team/${teamId}`)).data; } diff --git a/frontend/app/changelog/page.tsx b/frontend/app/changelog/page.tsx index 8e14c26c..308aaa5b 100644 --- a/frontend/app/changelog/page.tsx +++ b/frontend/app/changelog/page.tsx @@ -36,7 +36,7 @@ async function markdownToHtml(md: string): Promise { return String(file); } -// determines which version number was incremented in a release (e.g. v1.2.3.4 -> v1.3.0.0 is a level 2 bump) +// determines which version number was incremented in a release (e.g. v1.2.3.4 -> v1.3.0.0 is a level 2 bump). function bumpLevel(curr: string, prev?: string): 1 | 2 | 3 | 4 { if (!prev) return 4; diff --git a/frontend/app/events/[id]/bracket/graphView.tsx b/frontend/app/events/[id]/bracket/graphView.tsx index 0a5eb726..2b2f9a19 100644 --- a/frontend/app/events/[id]/bracket/graphView.tsx +++ b/frontend/app/events/[id]/bracket/graphView.tsx @@ -129,7 +129,7 @@ export default function GraphView({ }; newNodes.push({ - id: match.id, + id: match.id ?? `match-${round}-${index}`, type: "matchNode", position: { x: coord.x, y: coord.y }, data: { @@ -137,7 +137,10 @@ export default function GraphView({ width: MATCH_WIDTH, height: MATCH_HEIGHT, onClick: (clickedMatch: Match) => { - if (match.state === MatchState.FINISHED || isEventAdmin) + if ( + (match.state === MatchState.FINISHED || isEventAdmin) && + clickedMatch.id + ) router.push(`/events/${eventId}/match/${clickedMatch.id}`); }, }, @@ -151,7 +154,7 @@ export default function GraphView({ }; newNodes.push({ - id: placementMatch.id, + id: placementMatch.id ?? `placement-${round}`, type: "matchNode", position: { x: placementCoord.x, y: placementCoord.y }, data: { @@ -160,8 +163,9 @@ export default function GraphView({ height: MATCH_HEIGHT, onClick: (clickedMatch: Match) => { if ( - placementMatch.state === MatchState.FINISHED || - isEventAdmin + (placementMatch.state === MatchState.FINISHED || + isEventAdmin) && + clickedMatch.id ) router.push(`/events/${eventId}/match/${clickedMatch.id}`); }, diff --git a/frontend/app/events/[id]/dashboard/dashboard.tsx b/frontend/app/events/[id]/dashboard/dashboard.tsx index 8cb8aeda..8a9af351 100644 --- a/frontend/app/events/[id]/dashboard/dashboard.tsx +++ b/frontend/app/events/[id]/dashboard/dashboard.tsx @@ -18,6 +18,7 @@ import { import { lockEvent, unlockEvent } from "@/app/actions/team"; import { + revealAllMatches, startSwissMatches, startTournamentMatches, } from "@/app/actions/tournament"; @@ -170,6 +171,23 @@ export function DashboardPage({ eventId }: DashboardPageProps) { }, }); + const revealMatchesMutation = useMutation({ + mutationFn: async (phase: string) => { + const result = await revealAllMatches(eventId, phase); + if (isActionError(result)) { + throw new Error(result.error); + } + return result; + }, + onSuccess: async () => { + toast.success("Matches revealed."); + await queryClient.invalidateQueries({ queryKey: ["event", eventId] }); + }, + onError: (e: any) => { + toast.error(e.message || "Failed to reveal matches."); + }, + }); + const setTeamsLockDateMutation = useMutation({ mutationFn: async (lockDate: number | null) => { const result = await setEventTeamsLockDate(eventId, lockDate); @@ -535,47 +553,82 @@ export function DashboardPage({ eventId }: DashboardPageProps) { Immediate actions for running the event. - -
- - - - + +
+

+ Repository Management +

+
+ + +
-
-

+
+

+ Group Phase +

+
+ + +
+
+ +
+

+ Tournament Phase +

+
+ + +
+
+ +
+

Scheduling Auto-Lock

diff --git a/frontend/app/events/[id]/groups/graphView.tsx b/frontend/app/events/[id]/groups/graphView.tsx index 479f313e..e30b04f0 100644 --- a/frontend/app/events/[id]/groups/graphView.tsx +++ b/frontend/app/events/[id]/groups/graphView.tsx @@ -29,13 +29,11 @@ export default function GraphView({ const eventId = useParams().id as string; useEffect(() => { - if (!matches || matches.length === 0) - return; + if (!matches || matches.length === 0) return; const matchesByRound = matches.reduce( (acc, match) => { - if (!acc[match.round]) - acc[match.round] = []; + if (!acc[match.round]) acc[match.round] = []; acc[match.round].push(match); return acc; }, @@ -83,14 +81,14 @@ export default function GraphView({ // Add match nodes roundMatches.forEach((match, matchIndex) => { - const xPos - = roundIndex * COLUMN_WIDTH - + PADDING - + (COLUMN_WIDTH - MATCH_WIDTH - PADDING * 2) / 2; + const xPos = + roundIndex * COLUMN_WIDTH + + PADDING + + (COLUMN_WIDTH - MATCH_WIDTH - PADDING * 2) / 2; const yPos = (matchIndex + 1) * ROW_HEIGHT + PADDING + 20; // +60 for header space newNodes.push({ - id: match.id, + id: match.id ?? `match-${round}-${matchIndex}`, type: "matchNode", position: { x: xPos, y: yPos }, data: { @@ -98,7 +96,10 @@ export default function GraphView({ width: MATCH_WIDTH, height: MATCH_HEIGHT, onClick: (clickedMatch: Match) => { - if (match.state === MatchState.FINISHED || eventAdmin) + if ( + (match.state === MatchState.FINISHED || eventAdmin) && + clickedMatch.id + ) router.push(`/events/${eventId}/match/${clickedMatch.id}`); }, }, diff --git a/frontend/app/events/[id]/teams/[teamId]/TeamMatchHistory.tsx b/frontend/app/events/[id]/teams/[teamId]/TeamMatchHistory.tsx new file mode 100644 index 00000000..78d18885 --- /dev/null +++ b/frontend/app/events/[id]/teams/[teamId]/TeamMatchHistory.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import QueueMatchesList from "@/components/QueueMatchesList"; +import { MatchPhase } from "@/app/actions/tournament-model"; +import type { Match } from "@/app/actions/tournament-model"; + +interface TeamMatchHistoryProps { + eventId: string; + matches: Match[]; +} + +export default function TeamMatchHistory({ + eventId, + matches, +}: TeamMatchHistoryProps) { + return ( + +
+ + All Matches + Queue + Swiss + Tournament + +
+ + + + + + m.phase === MatchPhase.QUEUE)} + isInsideCard + /> + + + m.phase === MatchPhase.SWISS)} + isInsideCard + /> + + + m.phase === MatchPhase.ELIMINATION)} + isInsideCard + /> + +
+ ); +} diff --git a/frontend/app/events/[id]/teams/[teamId]/page.tsx b/frontend/app/events/[id]/teams/[teamId]/page.tsx index 513c8973..d368f246 100644 --- a/frontend/app/events/[id]/teams/[teamId]/page.tsx +++ b/frontend/app/events/[id]/teams/[teamId]/page.tsx @@ -4,10 +4,10 @@ import React from "react"; import { isActionError } from "@/app/actions/errors"; import { getTeamById, getTeamMembers } from "@/app/actions/team"; import { getMatchesForTeam } from "@/app/actions/tournament"; -import QueueMatchesList from "@/components/QueueMatchesList"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardHeader } from "@/components/ui/card"; import { History } from "lucide-react"; import BackButton from "./BackButton"; +import TeamMatchHistory from "./TeamMatchHistory"; import TeamUserTable from "./TeamUserTable"; export async function generateMetadata({ @@ -50,16 +50,12 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) { const matches = await getMatchesForTeam(teamId); return ( -
+
-

- Team - {" "} - {teamInfo.name} -

+

Team {teamInfo.name}

@@ -69,18 +65,12 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
-

Match History

+

+ Match History +

- - - +
); diff --git a/frontend/components/QueueMatchesList.tsx b/frontend/components/QueueMatchesList.tsx index 21116242..df4fafe2 100644 --- a/frontend/components/QueueMatchesList.tsx +++ b/frontend/components/QueueMatchesList.tsx @@ -1,6 +1,6 @@ import type { Match } from "@/app/actions/tournament-model"; import Link from "next/link"; -import { MatchState } from "@/app/actions/tournament-model"; +import { MatchState, MatchPhase } from "@/app/actions/tournament-model"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; @@ -9,19 +9,21 @@ import { cn } from "@/lib/utils"; export default function QueueMatchesList(props: { eventId: string; matches: Match[]; - hideUuid?: boolean; - hideReplay?: boolean; isInsideCard?: boolean; }) { - const { eventId, matches, hideUuid, hideReplay, isInsideCard } = props; + const { eventId, matches, isInsideCard } = props; if (!matches || matches.length === 0) { return ( -
-

No past matches found

+
+

+ No past matches found +

); } @@ -32,26 +34,48 @@ export default function QueueMatchesList(props: { const content = (
{/* Match Meta */} -
- - {match.state} - +
+
+ + {match.state} + + + {match.phase === MatchPhase.QUEUE && "Queue"} + {match.phase === MatchPhase.SWISS && "Swiss"} + {match.phase === MatchPhase.ELIMINATION && "Tournament"} + +
{new Date(match.createdAt).toLocaleDateString()} - {new Date(match.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + {new Date(match.createdAt).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })}
@@ -74,7 +98,9 @@ export default function QueueMatchesList(props: { href={`/events/${eventId}/teams/${match.teams[0].id}`} className={cn( "text-sm sm:text-base font-semibold truncate hover:underline hover:text-primary transition-colors text-right w-full", - match.winner?.id === match.teams[0].id ? "text-foreground" : "text-muted-foreground", + match.winner?.id === match.teams[0].id + ? "text-foreground" + : "text-muted-foreground", )} > {match.teams[0].name} @@ -83,21 +109,31 @@ export default function QueueMatchesList(props: { )} )} - - {match.results.find(r => r.team?.id === match.teams[0].id)?.score ?? 0} + + {match.results.find( + (r) => r.team?.id === match.teams[0].id, + )?.score ?? 0} ) : ( - Unknown + + Unknown + )}
{/* VS */}
- VS + + VS +
{/* Team 2 */} @@ -115,7 +151,9 @@ export default function QueueMatchesList(props: { href={`/events/${eventId}/teams/${match.teams[1].id}`} className={cn( "text-sm sm:text-base font-semibold truncate hover:underline hover:text-primary transition-colors text-left w-full", - match.winner?.id === match.teams[1].id ? "text-foreground" : "text-muted-foreground", + match.winner?.id === match.teams[1].id + ? "text-foreground" + : "text-muted-foreground", )} > {match.winner?.id === match.teams[1].id && ( @@ -124,33 +162,47 @@ export default function QueueMatchesList(props: { {match.teams[1].name} )} - - {match.results.find(r => r.team?.id === match.teams[1].id)?.score ?? 0} + + {match.results.find( + (r) => r.team?.id === match.teams[1].id, + )?.score ?? 0} ) : ( - Unknown + + Unknown + )}
{/* Replay */} - {!hideReplay && match.id && ( -
+
+ {match.id && ( - -
- )} + )} +
); @@ -167,9 +219,7 @@ export default function QueueMatchesList(props: { key={match.id || index} className="overflow-hidden border shadow-sm" > - - {content} - + {content} ); })} diff --git a/frontend/components/match/MatchNode.tsx b/frontend/components/match/MatchNode.tsx index f7c7bd39..3ea5558b 100644 --- a/frontend/components/match/MatchNode.tsx +++ b/frontend/components/match/MatchNode.tsx @@ -112,46 +112,42 @@ function MatchNode({ data }: MatchNodeProps) { {/* Match info */}
- Match - {" "} - {match.id.slice(0, 4)} + Match {match.id?.slice(0, 4) ?? "TBD"}
{/* Teams */}
- {match.teams - && match.state === MatchState.FINISHED - && match.teams.length > 0 - ? ( - match.teams.map((team, index) => ( -
- - {formatTeamName(team.name)} - {team.id === match.winner?.id && ( - 👑 - )} + {match.teams && + match.state === MatchState.FINISHED && + match.teams.length > 0 ? ( + match.teams.map((team, index) => ( +
+ + {formatTeamName(team.name)} + {team.id === match.winner?.id && ( + 👑 + )} + + {match.state === MatchState.FINISHED && + team.score !== undefined && ( + + {(match.results || []).find( + (result) => result?.team?.id === team.id, + )?.score ?? team.score} - {match.state === MatchState.FINISHED - && team.score !== undefined && ( - - {(match.results || []).find( - result => result?.team?.id === team.id, - )?.score || team.score} - - )} -
- )) - ) - : ( -
- TBD -
- )} + )} +
+ )) + ) : ( +
+ TBD +
+ )}
diff --git a/frontend/components/ui/tabs.tsx b/frontend/components/ui/tabs.tsx index 894161f4..1bf84c1f 100644 --- a/frontend/components/ui/tabs.tsx +++ b/frontend/components/ui/tabs.tsx @@ -2,36 +2,64 @@ import * as TabsPrimitive from "@radix-ui/react-tabs"; import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const Tabs = TabsPrimitive.Root; +const tabsListVariants = cva( + "inline-flex items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", + { + variants: { + variant: { + default: "h-9", + line: "bg-transparent h-auto p-0 gap-6 rounded-none", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + const TabsList = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => ( )); TabsList.displayName = TabsPrimitive.List.displayName; +const tabsTriggerVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "rounded-md data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", + line: "rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none font-bold uppercase tracking-widest px-0 py-2", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + const TabsTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => ( ));