UNPKG

react-bracket-ui

Version:

A modern, feature-rich React component library for displaying single-elimination tournament brackets with drag-drop, zoom/pan, and error validation

671 lines (555 loc) 16 kB
# react-bracket-ui <div align="center"> <h3>🏆 Professional React Tournament Bracket Component Library</h3> <p>A modern, feature-rich React component for displaying single-elimination tournament brackets</p> </div> ## ✨ Features - **Pure Display Component** - Only displays bracket data passed from FE - **React 19 Ready** - Built with TypeScript and latest React features - **Zoom & Pan** - Interactive zoom/pan for better bracket navigation - **Drag & Drop** - Rearrange teams between matches with intuitive drag-and-drop - **Error Validation** - Visual error indicators (red borders) for invalid matches - **Callbacks** - Emit events when users modify bracket positions - **Customizable** - Extensive styling and color customization options - **Optimized** - Performance-optimized with React.memo and useMemo - **TypeScript** - Full type safety with comprehensive TypeScript definitions - **Zero Config** - Works out-of-the-box with sensible defaults ## 📦 Installation ```bash npm install react-bracket-ui # or yarn add react-bracket-ui # or pnpm add react-bracket-ui ``` ## 🚀 Quick Start ```tsx import React from 'react'; import { Bracket } from 'react-bracket-ui'; import type { Match } from 'react-bracket-ui'; const matches: Match[] = [ { id: 1, round: 1, participant1: { id: 'p1', name: 'Team A', score: 2 }, participant2: { id: 'p2', name: 'Team B', score: 1 }, winner: 'p1', nextMatchId: 3 }, { id: 2, round: 1, participant1: { id: 'p3', name: 'Team C', score: 3 }, participant2: { id: 'p4', name: 'Team D', score: 0 }, winner: 'p3', nextMatchId: 3 }, { id: 3, round: 2, participant1: { id: 'p1', name: 'Team A' }, participant2: { id: 'p3', name: 'Team C' } } ]; function App() { return ( <Bracket matches={matches} showRoundNames={true} /> ); } ``` ## 📖 API Reference ### BracketProps | Prop | Type | Default | Description | |------|------|---------|-------------| | `matches` | `Match[]` | **required** | Array of match objects | | `className` | `string` | `undefined` | CSS class name | | `style` | `React.CSSProperties` | `undefined` | Inline styles | | `enableDragDrop` | `boolean` | `false` | Enable drag & drop functionality | | `onBracketChange` | `(event: BracketChangeEvent) => void` | `undefined` | Callback when bracket changes | | `enableZoomPan` | `boolean` | `false` | Enable zoom/pan controls | | `minZoom` | `number` | `0.5` | Minimum zoom level | | `maxZoom` | `number` | `3` | Maximum zoom level | | `initialZoom` | `number` | `1` | Initial zoom level | | `errors` | `BracketError[]` | `undefined` | Custom validation errors | | `onErrorClick` | `(error: BracketError) => void` | `undefined` | Callback when error is clicked | | `showRoundNames` | `boolean` | `true` | Show round names (Final, Semi-Final, etc.) | | `roundNames` | `Record<number, string>` | `undefined` | Custom round names | | `matchWidth` | `number` | `220` | Width of match box (px) | | `matchHeight` | `number` | `100` | Height of match box (px) | | `gap` | `number` | `20` | Gap between rounds (px) | | `colors` | `ColorConfig` | default theme | Custom color scheme | ### Match Interface ```typescript interface Match { id: string | number; participant1?: Participant | null; participant2?: Participant | null; winner?: string | number | null; round: number; matchNumber?: number; nextMatchId?: string | number | null; hasError?: boolean; errorMessage?: string; } ``` ### Participant Interface ```typescript interface Participant { id: string | number; name: string; score?: number; seed?: number; } ``` ## 🎨 Advanced Examples ### Example 1: Basic Bracket ```tsx import { Bracket } from 'react-bracket-ui'; function BasicBracket() { const matches = [ { id: 1, round: 1, participant1: { id: 'p1', name: 'Player 1', score: 3 }, participant2: { id: 'p2', name: 'Player 2', score: 1 }, winner: 'p1' } ]; return <Bracket matches={matches} />; } ``` ### Example 2: With Drag & Drop ```tsx import { Bracket, BracketChangeEvent } from 'react-bracket-ui'; import { useState } from 'react'; function DraggableBracket() { const [matches, setMatches] = useState([ { id: 1, round: 1, participant1: { id: 'p1', name: 'Team A' }, participant2: { id: 'p2', name: 'Team B' } }, { id: 2, round: 1, participant1: { id: 'p3', name: 'Team C' }, participant2: { id: 'p4', name: 'Team D' } } ]); const handleBracketChange = (event: BracketChangeEvent) => { console.log('Bracket changed:', event); setMatches(event.matches); // Send to backend // await api.updateBracket(event.matches); }; return ( <Bracket matches={matches} enableDragDrop={true} onBracketChange={handleBracketChange} /> ); } ``` ### Example 3: With Zoom & Pan ```tsx import { Bracket } from 'react-bracket-ui'; function ZoomableBracket() { return ( <Bracket matches={matches} enableZoomPan={true} minZoom={0.3} maxZoom={5} initialZoom={1} style={{ height: '800px' }} /> ); } ``` ### Example 4: Custom Colors & Styling ```tsx import { Bracket } from 'react-bracket-ui'; function CustomBracket() { return ( <Bracket matches={matches} colors={{ primary: '#ff6b6b', secondary: '#4ecdc4', background: '#1a1a2e', error: '#ff0000', warning: '#ffa500', winner: '#90ee90' }} matchWidth={250} matchHeight={120} gap={30} style={{ backgroundColor: '#0f0f23', border: '2px solid #4ecdc4' }} /> ); } ``` ### Example 5: With Error Validation ```tsx import { Bracket, BracketError, validateBracket } from 'react-bracket-ui'; import { useMemo } from 'react'; function ValidatedBracket() { const matches = [ { id: 1, round: 1, participant1: { id: 'p1', name: 'Team A' }, participant2: { id: 'p2', name: 'Team B' }, winner: 'p999' // Invalid winner ID! } ]; // Automatic validation const errors = useMemo(() => validateBracket(matches), [matches]); // Or custom validation const customErrors: BracketError[] = [ { matchId: 1, message: 'This match has scheduling conflict', type: 'warning' } ]; return ( <Bracket matches={matches} errors={[...errors, ...customErrors]} onErrorClick={(error) => { alert(`Error in match ${error.matchId}: ${error.message}`); }} /> ); } ``` ### Example 6: Custom Round Names ```tsx import { Bracket } from 'react-bracket-ui'; function CustomRoundNames() { return ( <Bracket matches={matches} showRoundNames={true} roundNames={{ 1: 'Round of 16', 2: 'Quarter Finals', 3: 'Semi Finals', 4: 'Grand Final' }} /> ); } ``` ### Example 7: Complete Tournament Example ```tsx import { Bracket, BracketChangeEvent } from 'react-bracket-ui'; import { useState, useCallback } from 'react'; function TournamentBracket() { const [matches, setMatches] = useState([ // Round 1 { id: 1, round: 1, matchNumber: 1, participant1: { id: 't1', name: 'Team Alpha', seed: 1, score: 3 }, participant2: { id: 't8', name: 'Team Hotel', seed: 8, score: 0 }, winner: 't1', nextMatchId: 5 }, { id: 2, round: 1, matchNumber: 2, participant1: { id: 't4', name: 'Team Delta', seed: 4, score: 2 }, participant2: { id: 't5', name: 'Team Echo', seed: 5, score: 1 }, winner: 't4', nextMatchId: 5 }, { id: 3, round: 1, matchNumber: 3, participant1: { id: 't2', name: 'Team Bravo', seed: 2, score: 3 }, participant2: { id: 't7', name: 'Team Golf', seed: 7, score: 2 }, winner: 't2', nextMatchId: 6 }, { id: 4, round: 1, matchNumber: 4, participant1: { id: 't3', name: 'Team Charlie', seed: 3, score: 1 }, participant2: { id: 't6', name: 'Team Foxtrot', seed: 6, score: 3 }, winner: 't6', nextMatchId: 6 }, // Semi-Finals { id: 5, round: 2, matchNumber: 5, participant1: { id: 't1', name: 'Team Alpha' }, participant2: { id: 't4', name: 'Team Delta' }, nextMatchId: 7 }, { id: 6, round: 2, matchNumber: 6, participant1: { id: 't2', name: 'Team Bravo' }, participant2: { id: 't6', name: 'Team Foxtrot' }, nextMatchId: 7 }, // Final { id: 7, round: 3, matchNumber: 7, participant1: null, participant2: null } ]); const handleBracketChange = useCallback(async (event: BracketChangeEvent) => { console.log('Bracket updated:', event); setMatches(event.matches); // Sync with backend try { // await updateTournamentBracket(event.matches); console.log('Bracket saved successfully'); } catch (error) { console.error('Failed to save bracket:', error); } }, []); return ( <div style={{ padding: '20px' }}> <h1>World Championship 2025</h1> <Bracket matches={matches} enableDragDrop={true} enableZoomPan={true} onBracketChange={handleBracketChange} showRoundNames={true} roundNames={{ 1: 'Quarter Finals', 2: 'Semi Finals', 3: 'Grand Final' }} colors={{ primary: '#1976d2', winner: '#c8e6c9' }} matchWidth={240} gap={25} style={{ height: '700px' }} /> </div> ); } ``` ## 🛠️ Utility Functions ### validateBracket Validates bracket structure and returns errors: ```tsx import { validateBracket } from 'react-bracket-ui'; const errors = validateBracket(matches); // Returns BracketError[] with validation issues ``` ### hasMatchError Check if a specific match has errors: ```tsx import { hasMatchError } from 'react-bracket-ui'; const hasError = hasMatchError(matchId, errors); ``` ### getMatchErrorMessage Get error message for a specific match: ```tsx import { getMatchErrorMessage } from 'react-bracket-ui'; const message = getMatchErrorMessage(matchId, errors); ``` ## 🎯 Use Cases - **Tournament Management Systems** - Display and manage tournament brackets - **Sports Applications** - Visualize competition brackets - **Gaming Platforms** - Show esports tournament structures - **Event Management** - Organize single-elimination events - **Admin Dashboards** - Allow admins to adjust bracket arrangements ## 🤝 Integration Examples ### With React Query ```tsx import { useQuery, useMutation } from '@tanstack/react-query'; import { Bracket, BracketChangeEvent } from 'react-bracket-ui'; function TournamentView({ tournamentId }: { tournamentId: string }) { const { data: matches } = useQuery({ queryKey: ['tournament', tournamentId], queryFn: () => fetchTournamentMatches(tournamentId) }); const mutation = useMutation({ mutationFn: (matches: Match[]) => updateTournament(tournamentId, matches) }); const handleChange = (event: BracketChangeEvent) => { mutation.mutate(event.matches); }; return ( <Bracket matches={matches || []} enableDragDrop={true} onBracketChange={handleChange} /> ); } ``` ### With Redux ```tsx import { useDispatch, useSelector } from 'react-redux'; import { Bracket } from 'react-bracket-ui'; import { updateBracketAction } from './store/tournamentSlice'; function ReduxBracket() { const dispatch = useDispatch(); const matches = useSelector(state => state.tournament.matches); return ( <Bracket matches={matches} enableDragDrop={true} onBracketChange={(event) => { dispatch(updateBracketAction(event.matches)); }} /> ); } ``` ## 📋 TypeScript Support Full TypeScript support with comprehensive type definitions: ```typescript import type { Match, Participant, BracketProps, BracketError, BracketChangeEvent, DragDropResult } from 'react-bracket-ui'; ``` ## 🔧 Troubleshooting ### Drag and Drop not working Make sure to: 1. Set `enableDragDrop={true}` 2. Provide `onBracketChange` callback 3. Update your state with new matches from the callback ### Zoom/Pan not working Ensure: 1. Set `enableZoomPan={true}` 2. Provide adequate height in `style` prop 3. Check if parent container has overflow issues ### Errors not showing Verify: 1. Matches have valid IDs 2. Using `validateBracket()` or providing `errors` prop 3. Check console for validation messages ## 🌟 Performance Tips 1. **Memoize matches array** - Prevent unnecessary re-renders 2. **Use stable IDs** - Don't use array indices as match IDs 3. **Optimize callbacks** - Wrap callbacks with `useCallback` 4. **Lazy load** - Load large brackets progressively ## 📄 License ISC License - see LICENSE file for details ## 👥 Author **thuatdt137** ## 🔗 Links - [GitHub Repository](https://github.com/thuatdt137/react-bracket-ui) - [npm Package](https://www.npmjs.com/package/react-bracket-ui) - [Report Issues](https://github.com/thuatdt137/react-bracket-ui/issues) ## 🙏 Contributing Contributions are welcome! Please feel free to submit a Pull Request. --- <div align="center"> Made with ❤️ for the tournament community </div> A React component library for displaying tournament brackets with TypeScript support. ## Installation ```bash npm install react-bracket-ui ``` ## Usage ```tsx import React from 'react'; import { Bracket, Match } from 'react-bracket-ui'; const matches: Match[] = [ { id: 1, participant1: { id: 1, name: "Player A", score: 3 }, participant2: { id: 2, name: "Player B", score: 1 }, winner: 1, round: 1, nextMatchId: 3 }, { id: 2, participant1: { id: 3, name: "Player C", score: 2 }, participant2: { id: 4, name: "Player D", score: 4 }, winner: 4, round: 1, nextMatchId: 3 }, { id: 3, participant1: { id: 1, name: "Player A" }, participant2: { id: 4, name: "Player D" }, round: 2 } ]; function App() { return ( <div> <h1>Tournament</h1> <Bracket matches={matches} /> </div> ); } ``` ## Props ### Bracket Component | Prop | Type | Default | Description | |------|------|---------|-------------| | `matches` | `Match[]` | required | Array of match objects | | `className` | `string` | undefined | CSS class name | | `style` | `React.CSSProperties` | undefined | Inline styles | ### Match Interface ```tsx interface Match { id: string | number; participant1?: { id: string | number; name: string; score?: number; }; participant2?: { id: string | number; name: string; score?: number; }; winner?: string | number; round: number; nextMatchId?: string | number; } ``` ## Features - TypeScript support - Responsive design - Customizable styling - Multiple rounds support - Winner highlighting - Score display - ESM and CommonJS support ## License ISC ## Author thuatdt137