diff --git a/api/README.md b/api/README.md index 9b1a97aa..25f33aa5 100644 --- a/api/README.md +++ b/api/README.md @@ -6,11 +6,11 @@ Main backend API service for the CORE game website. This NestJS service serves as the primary backend, handling: -- User authentication and management -- Team and event/tournament management -- Match results and statistics -- Database operations (PostgreSQL) -- RabbitMQ microservice communication +* User authentication and management +* Team and event/tournament management +* Match results and statistics +* Database operations (PostgreSQL) +* RabbitMQ microservice communication ## Getting Started @@ -58,47 +58,42 @@ brew install pnpm ### Production -- **Build:** `pnpm build` -- **Start:** `pnpm start:prod` +* **Build:** `pnpm build` +* **Start:** `pnpm start:prod` ## Environment Variables ### Database (Required) -- `DB_HOST` - PostgreSQL host -- `DB_PORT` - PostgreSQL port -- `DB_USER` - Database username -- `DB_PASSWORD` - Database password -- `DB_NAME` - Database name -- `DB_SCHEMA` - Database schema -- `DB_URL` - Alternative database connection URL overwrites the other database connection variables -- `DB_SSL_REQUIRED` - Enable SSL connection (true/false) +* `DB_HOST` - PostgreSQL host +* `DB_PORT` - PostgreSQL port +* `DB_USER` - Database username +* `DB_PASSWORD` - Database password +* `DB_NAME` - Database name +* `DB_SCHEMA` - Database schema +* `DB_URL` - Alternative database connection URL overwrites the other database connection variables +* `DB_SSL_REQUIRED` - Enable SSL connection (true/false) ### External Services (Required) -- `RABBITMQ_URL` - RabbitMQ connection URL -- `API_SECRET_ENCRYPTION_KEY` - Key for encrypting sensitive data +* `RABBITMQ_URL` - RabbitMQ connection URL +* `API_SECRET_ENCRYPTION_KEY` - Key for encrypting sensitive data ### Optional -- `PORT` - Server port (default: 4000) -- `NODE_ENV` - Environment (development/production) +* `PORT` - Server port (default: 4000) +* `NODE_ENV` - Environment (development/production) ## Database Management ### Migrations -- **Generate:** `pnpm migration:generate migration_name` +* **Generate:** `pnpm migration:generate migration_name` Compares your current TypeScript entities with the database and automatically generates the necessary SQL (e.g., adding or removing columns). **Use this for most schema changes.** -- **Create:** `pnpm migration:create migration_name` +* **Create:** `pnpm migration:create migration_name` Creates an empty migration template. **Use this only for manual SQL changes** (e.g., seeding data, creating complex views, or custom indexes) that TypeORM cannot detect automatically. -- **Run:** `pnpm migration:run-local` (local) / `pnpm migration:run` (production) -- **Revert:** `pnpm migration:revert-local` (local) / `pnpm migration:revert` (production) - -### Seeding - -- **Seed Users and Teams:** `pnpm seed:users ` - Generates 90 users and 30 teams (3 members each) and assigns them to the specified event. This is useful for testing group phases and bracket logic. +* **Run:** `pnpm migration:run-local` (local) / `pnpm migration:run` (production) +* **Revert:** `pnpm migration:revert-local` (local) / `pnpm migration:revert` (production) ## API Documentation @@ -109,10 +104,10 @@ When running in development mode, Swagger documentation is available at: This service runs as both: -- **REST API** - HTTP endpoints for frontend communication -- **Microservice** - RabbitMQ message consumer for: - - `game_results` - Match result processing - - `github-service-results` - GitHub operation results +* **REST API** - HTTP endpoints for frontend communication +* **Microservice** - RabbitMQ message consumer for: + * `game_results` - Match result processing + * `github-service-results` - GitHub operation results ## Environment Variables diff --git a/api/package.json b/api/package.json index fc8b31be..33c03961 100644 --- a/api/package.json +++ b/api/package.json @@ -25,8 +25,7 @@ "migration:generate": "sh -c 'npm run typeorm:ts -- -d ./typeOrm.config.ts migration:generate ./db/migrations/\"$0\"'", "migration:create": "sh -c 'npm run typeorm:ts -- migration:create ./db/migrations/\"$0\"'", "migration:revert": "npm run typeorm -- -d ./dist/typeOrm.config.js migration:revert", - "migration:revert-local": "npm run typeorm:ts -- -d ./typeOrm.config.ts migration:revert", - "seed:users": "ts-node -r tsconfig-paths/register src/scripts/seed-users-teams.ts" + "migration:revert-local": "npm run typeorm:ts -- -d ./typeOrm.config.ts migration:revert" }, "dependencies": { "@nestjs/common": "^11.1.12", diff --git a/api/src/scripts/seed-users-teams.ts b/api/src/scripts/seed-users-teams.ts deleted file mode 100644 index df36f6db..00000000 --- a/api/src/scripts/seed-users-teams.ts +++ /dev/null @@ -1,106 +0,0 @@ -import "reflect-metadata"; -import { DataSource, DataSourceOptions } from "typeorm"; -import { EventEntity } from "../event/entities/event.entity"; -import { - UserEntity, - UserEventPermissionEntity, - PermissionRole, -} from "../user/entities/user.entity"; -import { TeamEntity } from "../team/entities/team.entity"; -import { config } from "dotenv"; -import { DatabaseConfig } from "../DatabaseConfig"; -import { ConfigService } from "@nestjs/config"; -import { join } from "path"; - -config(); - -async function bootstrap() { - const eventId = process.argv[2]; - if (!eventId) { - console.error("Please provide an eventId as the first argument"); - process.exit(1); - } - - console.log("Connecting to database..."); - const configService = new ConfigService(); - const databaseConfig = new DatabaseConfig(configService); - const baseConfig = databaseConfig.getConfig(true); - - // Use a glob pattern that works with ts-node in development - const dataSource = new DataSource({ - ...baseConfig, - entities: [join(__dirname, "..", "**", "*.entity.ts")], - } as DataSourceOptions); - - await dataSource.initialize(); - console.log("Database connected!"); - - const userRepository = dataSource.getRepository(UserEntity); - const teamRepository = dataSource.getRepository(TeamEntity); - const eventRepository = dataSource.getRepository(EventEntity); - const permissionRepository = dataSource.getRepository( - UserEventPermissionEntity, - ); - - const event = await eventRepository.findOne({ where: { id: eventId } }); - if (!event) { - console.error(`Event with ID ${eventId} not found`); - await dataSource.destroy(); - process.exit(1); - } - - console.log( - `Seeding 90 users and 30 teams for event: ${event.name} (${event.id})`, - ); - - const users: UserEntity[] = []; - const now = Date.now(); - for (let i = 1; i <= 90; i++) { - const user = new UserEntity(); - user.githubId = `seed-user-${i}-${now}`; - user.githubAccessToken = "dummy-token"; - user.email = `user${i}@example.com`; - user.username = `seeduser${i}_${now.toString().slice(-5)}`; - user.name = `Seed User ${i}`; - user.profilePicture = `https://api.dicebear.com/7.x/avataaars/svg?seed=${user.username}`; - users.push(user); - } - - const savedUsers = await userRepository.save(users); - console.log(`Successfully saved 90 users`); - - // Add event permissions for these users - const permissions = savedUsers.map((user) => { - const perm = new UserEventPermissionEntity(); - perm.user = user; - perm.event = event; - perm.role = PermissionRole.USER; - return perm; - }); - await permissionRepository.save(permissions); - console.log(`Successfully added event permissions for 90 users`); - - const teams: TeamEntity[] = []; - for (let i = 1; i <= 30; i++) { - const team = new TeamEntity(); - team.name = `Seed Team ${i}`; - team.event = event; - - // Assign 3 users to each team - const teamUsers = savedUsers.slice((i - 1) * 3, i * 3); - team.users = teamUsers; - - teams.push(team); - } - - await teamRepository.save(teams); - console.log(`Successfully saved 30 teams and assigned users`); - - console.log("Seeding completed successfully!"); - await dataSource.destroy(); -} - -bootstrap().catch((err) => { - console.error("Error seeding data:", err); - process.exit(1); -}); diff --git a/frontend/app/events/[id]/bracket/actions.tsx b/frontend/app/events/[id]/bracket/actions.tsx index ce7834c0..76b5d948 100644 --- a/frontend/app/events/[id]/bracket/actions.tsx +++ b/frontend/app/events/[id]/bracket/actions.tsx @@ -1,30 +1,5 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; export default function Actions() { - const router = useRouter(); - const searchParams = useSearchParams(); - const isAdminView = searchParams.get("adminReveal") === "true"; - - return ( -
- { - const params = new URLSearchParams(searchParams.toString()); - params.set("adminReveal", value ? "true" : "false"); - router.replace(`?${params.toString()}`); - }} - /> - -
- ); + return <>; } diff --git a/frontend/app/events/[id]/bracket/graphView.tsx b/frontend/app/events/[id]/bracket/graphView.tsx index 643e7565..2b2f9a19 100644 --- a/frontend/app/events/[id]/bracket/graphView.tsx +++ b/frontend/app/events/[id]/bracket/graphView.tsx @@ -1,11 +1,12 @@ "use client"; -import type { Node, Edge } from "reactflow"; +import type { Node } from "reactflow"; import type { Match } from "@/app/actions/tournament-model"; import { useParams, useRouter } from "next/navigation"; import { useEffect } from "react"; import ReactFlow, { Background, useEdgesState, useNodesState } from "reactflow"; import { MatchState } from "@/app/actions/tournament-model"; import { MatchNode } from "@/components/match"; +import { Switch } from "@/components/ui/switch"; import "reactflow/dist/style.css"; const MATCH_WIDTH = 200; @@ -17,6 +18,25 @@ const nodeTypes = { matchNode: MatchNode, }; +function createTreeCoordinate(matchCount: number): { x: number; y: number }[] { + const coordinates: { x: number; y: number }[] = []; + const totalRounds = Math.ceil(Math.log2(matchCount + 1)); + + for (let round = 0; round < totalRounds; round++) { + const matchesInRound = 2 ** (totalRounds - round - 1); + const spacing = 2 ** round * VERTICAL_SPACING; + + for (let match = 0; match < matchesInRound; match++) { + const x = round * ROUND_SPACING; + const y = match * spacing + spacing / 2; + + coordinates.push({ x, y }); + } + } + + return coordinates; +} + function getTotalRounds(teamCount: number) { if (teamCount <= 1) return 1; return Math.ceil(Math.log2(teamCount)); @@ -34,34 +54,20 @@ export default function GraphView({ isAdminView: boolean; }) { const [nodes, setNodes, onNodesChange] = useNodesState([]); - const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [edges, _setEdges, onEdgesChange] = useEdgesState([]); const router = useRouter(); const eventId = useParams().id as string; useEffect(() => { - const newNodes: Node[] = []; - const newEdges: Edge[] = []; - const nodeIdsByRound: Map = new Map(); - if (!matches || matches.length === 0) { // Create placeholder nodes for visualization - const totalRounds = getTotalRounds(teamCount); - - for (let round = 0; round < totalRounds; round++) { - const matchesInRound = 2 ** (totalRounds - round - 1); - const spacing = 2 ** round * VERTICAL_SPACING; - const roundNodeIds: string[] = []; - - for (let match = 0; match < matchesInRound; match++) { - const id = `placeholder-${round}-${match}`; - const x = round * ROUND_SPACING; - const y = match * spacing + spacing / 2; - + const newNodes = createTreeCoordinate(teamCount / 2).map( + (coord, index): Node => { const placeholderMatch: Match = { id: ``, isRevealed: false, - round: round + 1, + round: index + 1, state: "PLANNED" as any, phase: "ELIMINATION" as any, createdAt: new Date().toISOString(), @@ -70,185 +76,152 @@ export default function GraphView({ results: [], }; - newNodes.push({ - id, + return { + id: index.toString(), type: "matchNode", - position: { x, y }, + position: { x: coord.x, y: coord.y }, data: { match: placeholderMatch, width: MATCH_WIDTH, height: MATCH_HEIGHT, - showTargetHandle: round > 0, - showSourceHandle: round < totalRounds - 1, }, - }); - roundNodeIds.push(id); - } - nodeIdsByRound.set(round, roundNodeIds); - } - } else { - // Create nodes from actual match data - const sortedMatches = [...matches].sort( - (a, b) => - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), + }; + }, ); - const matchesByRound = new Map(); - for (const match of sortedMatches) { - if (!matchesByRound.has(match.round)) { - matchesByRound.set(match.round, []); - } - matchesByRound.get(match.round)!.push(match); - } + setNodes(newNodes); + return; + } - const totalRounds = getTotalRounds(teamCount); - const lastRoundIndex = totalRounds - 1; - const roundKeys = Array.from(matchesByRound.keys()).sort((a, b) => a - b); + // Create nodes from actual match data + const sortedMatches = [...matches].sort( + (a, b) => + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), + ); + const matchesByRound = new Map(); + for (const match of sortedMatches) { + if (!matchesByRound.has(match.round)) { + matchesByRound.set(match.round, []); + } + matchesByRound.get(match.round)!.push(match); + } - for (const round of roundKeys) { - const roundIndex = round - 1; - const roundMatches = matchesByRound.get(round) || []; - const isLastRound = roundIndex === lastRoundIndex; + const totalRounds = getTotalRounds(teamCount); + const lastRound = totalRounds - 1; + const newNodes: Node[] = []; - const bracketMatches = isLastRound + const roundKeys = Array.from(matchesByRound.keys()).sort((a, b) => a - b); + for (const round of roundKeys) { + const roundMatches = matchesByRound.get(round) || []; + const placementMatch = + round === lastRound + ? roundMatches.find((match) => match.isPlacementMatch) + : undefined; + const bracketMatches = + round === lastRound ? roundMatches.filter((match) => !match.isPlacementMatch) : roundMatches; - - // Ensure bracket matches are sorted consistently for edge creation - bracketMatches.sort( - (a, b) => - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), - ); - - const spacing = 2 ** roundIndex * VERTICAL_SPACING; - const roundNodeIds: string[] = []; - - bracketMatches.forEach((match, index) => { - const id = match.id ?? `match-${roundIndex}-${index}`; - const coord = { - x: roundIndex * ROUND_SPACING, - y: index * spacing + spacing / 2, - }; - - newNodes.push({ - id, - type: "matchNode", - position: { x: coord.x, y: coord.y }, - data: { - match, - width: MATCH_WIDTH, - height: MATCH_HEIGHT, - showTargetHandle: roundIndex > 0, - showSourceHandle: roundIndex < lastRoundIndex, - onClick: (clickedMatch: Match) => { - if ( - (match.state === MatchState.FINISHED || isEventAdmin) && - clickedMatch.id - ) - router.push(`/events/${eventId}/match/${clickedMatch.id}`); - }, + const spacing = 2 ** round * VERTICAL_SPACING; + + bracketMatches.forEach((match, index) => { + const coord = { + x: round * ROUND_SPACING, + y: index * spacing + spacing / 2, + }; + + newNodes.push({ + id: match.id ?? `match-${round}-${index}`, + type: "matchNode", + position: { x: coord.x, y: coord.y }, + data: { + match, + width: MATCH_WIDTH, + height: MATCH_HEIGHT, + onClick: (clickedMatch: Match) => { + if ( + (match.state === MatchState.FINISHED || isEventAdmin) && + clickedMatch.id + ) + router.push(`/events/${eventId}/match/${clickedMatch.id}`); }, - }); - roundNodeIds.push(id); + }, }); - nodeIdsByRound.set(roundIndex, roundNodeIds); - - const placementMatch = isLastRound - ? roundMatches.find((match) => match.isPlacementMatch) - : undefined; - if (placementMatch) { - const placementId = placementMatch.id ?? `placement-${roundIndex}`; - const placementCoord = { - x: roundIndex * ROUND_SPACING, - y: spacing / 2 + VERTICAL_SPACING * 2, - }; + }); - newNodes.push({ - id: placementId, - type: "matchNode", - position: { x: placementCoord.x, y: placementCoord.y }, - data: { - match: placementMatch, - width: MATCH_WIDTH, - height: MATCH_HEIGHT, - showTargetHandle: false, - showSourceHandle: false, - onClick: (clickedMatch: Match) => { - if ( - (placementMatch.state === MatchState.FINISHED || - isEventAdmin) && - clickedMatch.id - ) - router.push(`/events/${eventId}/match/${clickedMatch.id}`); - }, + if (placementMatch) { + const placementCoord = { + x: round * ROUND_SPACING, + y: spacing / 2 + VERTICAL_SPACING * 2, + }; + + newNodes.push({ + id: placementMatch.id ?? `placement-${round}`, + type: "matchNode", + position: { x: placementCoord.x, y: placementCoord.y }, + data: { + match: placementMatch, + width: MATCH_WIDTH, + height: MATCH_HEIGHT, + onClick: (clickedMatch: Match) => { + if ( + (placementMatch.state === MatchState.FINISHED || + isEventAdmin) && + clickedMatch.id + ) + router.push(`/events/${eventId}/match/${clickedMatch.id}`); }, - }); - } + }, + }); } } - // Generate edges between rounds - const rounds = Array.from(nodeIdsByRound.keys()).sort((a, b) => a - b); - for (let i = 0; i < rounds.length - 1; i++) { - const currentRound = rounds[i]; - const nextRound = rounds[i + 1]; - const currentNodes = nodeIdsByRound.get(currentRound) || []; - const nextNodes = nodeIdsByRound.get(nextRound) || []; - - currentNodes.forEach((nodeId, index) => { - const targetIndex = Math.floor(index / 2); - if (nextNodes[targetIndex]) { - newEdges.push({ - id: `edge-${currentRound}-${index}`, - source: nodeId, - target: nextNodes[targetIndex], - type: "smoothstep", - animated: false, - style: { - stroke: "#64748b", // Muted slate color - strokeWidth: 2, - opacity: 0.5, - }, - }); - } - }); - } - setNodes(newNodes); - setEdges(newEdges); - }, [matches, teamCount, isEventAdmin, router, eventId, setNodes, setEdges]); + }, [matches, teamCount, isEventAdmin, router, eventId, setNodes]); return ( -
- - - +
+
+ + {isEventAdmin && ( +
+ Toggle admin view + { + const params = new URLSearchParams(window.location.search); + params.set("adminReveal", value ? "true" : "false"); + router.replace(`?${params.toString()}`); + }} + defaultChecked={isAdminView} + /> +
+ )} + + + +
); } diff --git a/frontend/app/events/[id]/bracket/page.tsx b/frontend/app/events/[id]/bracket/page.tsx index caf7a029..19f03831 100644 --- a/frontend/app/events/[id]/bracket/page.tsx +++ b/frontend/app/events/[id]/bracket/page.tsx @@ -33,32 +33,18 @@ export default async function page({ const teamCount = await getTournamentTeamCount(eventId); return ( -
-
-
-

- Tournament Tree -

-

- Follow the elimination bracket to see which teams advance and - ultimately compete in the finals. -

-
- {eventAdmin && ( -
- -
- )} -
- -
- +
+
+
+

Tournament Tree

+

+
); } diff --git a/frontend/app/events/[id]/groups/actions.tsx b/frontend/app/events/[id]/groups/actions.tsx index d9ed64ff..76b5d948 100644 --- a/frontend/app/events/[id]/groups/actions.tsx +++ b/frontend/app/events/[id]/groups/actions.tsx @@ -1,30 +1,5 @@ "use client"; -import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; export default function Actions() { - const router = useRouter(); - const searchParams = useSearchParams(); - const isAdminView = searchParams.get("adminReveal") === "true"; - - return ( -
- { - const params = new URLSearchParams(searchParams.toString()); - params.set("adminReveal", value ? "true" : "false"); - router.replace(`?${params.toString()}`); - }} - /> - -
- ); + return <>; } diff --git a/frontend/app/events/[id]/groups/graphView.tsx b/frontend/app/events/[id]/groups/graphView.tsx index ed886131..e30b04f0 100644 --- a/frontend/app/events/[id]/groups/graphView.tsx +++ b/frontend/app/events/[id]/groups/graphView.tsx @@ -10,15 +10,8 @@ import { Switch } from "@/components/ui/switch"; import "reactflow/dist/style.css"; // Custom node types for ReactFlow -const RoundNode = ({ data }: { data: { label: string } }) => ( -
- {data.label} -
-); - const nodeTypes = { matchNode: MatchNode, - roundNode: RoundNode, }; export default function GraphView({ @@ -53,11 +46,11 @@ export default function GraphView({ const newNodes: Node[] = []; - const COLUMN_WIDTH = 320; + const COLUMN_WIDTH = 300; const ROW_HEIGHT = 130; const PADDING = 20; - const MATCH_WIDTH = 280; - const MATCH_HEIGHT = 100; + const MATCH_WIDTH = 250; + const MATCH_HEIGHT = 80; rounds.forEach((round, roundIndex) => { const roundMatches = matchesByRound[round]; @@ -65,7 +58,6 @@ export default function GraphView({ // Add round header newNodes.push({ id: `round-${round}`, - type: "roundNode", position: { x: roundIndex * COLUMN_WIDTH + PADDING, y: PADDING, @@ -75,7 +67,13 @@ export default function GraphView({ }, style: { width: COLUMN_WIDTH - PADDING * 2, - height: 60, + height: 40, + textAlign: "center", + fontWeight: "bold", + padding: "10px", + backgroundColor: "#f1f5f9", + border: "2px solid #cbd5e1", + borderRadius: "8px", }, draggable: false, selectable: false, @@ -87,7 +85,7 @@ export default function GraphView({ roundIndex * COLUMN_WIDTH + PADDING + (COLUMN_WIDTH - MATCH_WIDTH - PADDING * 2) / 2; - const yPos = (matchIndex + 1) * ROW_HEIGHT + PADDING + 20; + const yPos = (matchIndex + 1) * ROW_HEIGHT + PADDING + 20; // +60 for header space newNodes.push({ id: match.id ?? `match-${round}-${matchIndex}`, @@ -110,37 +108,36 @@ export default function GraphView({ }); setNodes(newNodes); - }, [matches, eventAdmin, eventId, router]); + }, [matches]); return ( -
+
+ {eventAdmin && ( +
+ Toggle admin view + { + const params = new URLSearchParams(window.location.search); + params.set("adminReveal", value ? "true" : "false"); + router.replace(`?${params.toString()}`); + }} + defaultChecked={isAdminView} + /> +
+ )} - +
); diff --git a/frontend/app/events/[id]/groups/page.tsx b/frontend/app/events/[id]/groups/page.tsx index ee4e15e3..187b2901 100644 --- a/frontend/app/events/[id]/groups/page.tsx +++ b/frontend/app/events/[id]/groups/page.tsx @@ -26,31 +26,20 @@ export default async function page({ } return ( -
-
-
-

- Group Phase -

-

- In the group phase, teams compete using the Swiss tournament system, - with rankings determined by the Buchholz scoring system. -

-
- {eventAdmin && ( -
- -
- )} -
- -
- +
+
+
+

Group phase

+

+ In the group phase, teams compete using the Swiss tournament system, + with rankings determined by the Buchholz scoring system. +

+
); } diff --git a/frontend/components/match/MatchNode.tsx b/frontend/components/match/MatchNode.tsx index d1c85859..3ea5558b 100644 --- a/frontend/components/match/MatchNode.tsx +++ b/frontend/components/match/MatchNode.tsx @@ -4,16 +4,12 @@ import type { Match } from "@/app/actions/tournament-model"; import { motion } from "framer-motion"; import { memo } from "react"; import { MatchState } from "@/app/actions/tournament-model"; -import { useParams, useRouter } from "next/navigation"; -import { Handle, Position } from "reactflow"; interface MatchNodeData { match: Match; width?: number; height?: number; onClick?: (match: Match) => void; - showTargetHandle?: boolean; - showSourceHandle?: boolean; } interface MatchNodeProps { @@ -63,20 +59,9 @@ function getMatchStateIcon(state: MatchState) { } function MatchNode({ data }: MatchNodeProps) { - const { - match, - width = 200, - height = 80, - onClick, - showTargetHandle = false, - showSourceHandle = false, - } = data; + const { match, width = 200, height = 80, onClick } = data; const styles = getMatchStateStyles(match.state); const icon = getMatchStateIcon(match.state); - const router = useRouter(); - const params = useParams<{ id?: string }>(); - const rawId = params?.id; - const eventId = rawId ?? ""; const handleClick = () => { onClick?.(match); @@ -102,23 +87,6 @@ function MatchNode({ data }: MatchNodeProps) { whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > - {showTargetHandle && ( - - )} - {showSourceHandle && ( - - )} - {/* Animated progress indicator for IN_PROGRESS matches */} {match.state === MatchState.IN_PROGRESS && ( -
- { - e.stopPropagation(); - if (team.id) { - router.push(`/events/${eventId}/teams/${team.id}`); - } - }} - > - {formatTeamName(team.name)} - + + {formatTeamName(team.name)} {team.id === match.winner?.id && ( 👑 )} -
+ {match.state === MatchState.FINISHED && team.score !== undefined && ( diff --git a/frontend/components/team/TeamCreationSection.tsx b/frontend/components/team/TeamCreationSection.tsx index 2cd5ff21..2e62b99f 100644 --- a/frontend/components/team/TeamCreationSection.tsx +++ b/frontend/components/team/TeamCreationSection.tsx @@ -22,34 +22,24 @@ export function TeamCreationSection({ return ( - - Create Your Team - + Create Your Team -
{ - e.preventDefault(); - if (newTeamName && !validationError && !isLoading) { - handleCreateTeam(); - } - }} - className="flex flex-row gap-2" - > +
setNewTeamName(e.target.value)} + onChange={e => setNewTeamName(e.target.value)} /> - +
{validationError && (
{validationError}