@g-loot/react-tournament-brackets

中文文档
1.0.31-rc发布于 2023/12/04ISCTypeScriptESM + CJS近 30 天下载 1.3万

A react component to visualize bracket leaderboards

原始使用文档
文档目录18 节
  1. React Tournament Brackets
  2. About The Project
  3. Built With
  4. Getting Started
  5. Installation
  6. Basic Usage
  7. Basics of the library
  8. Using the components
  9. Data structures
  10. Theming and Styling
  11. If themes aren't enough for you you can always supply your …
  12. License
  13. Contributing
  14. Contributer Testing Guide
  15. Unit tests and linting
  16. Checking that storybook works
  17. Checking that the package works when consumed by a test pro…
  18. Acknowledgements

Contributors Forks Stargazers Issues


React Tournament Brackets

A straightforward implementation of single eliminations and double eliminations brackets in react
Explore the storybook »

View Live Demo · Report Bug · Request Feature

Table of Contents
  1. About The Project
  2. Getting Started
  3. Contributing
  4. <li><a href="#acknowledgements">Acknowledgements</a></li>
    

About The Project

  • Screenshot Of single elimination bracket out of the box Single elimination screenshot
  • Screenshot Of double elimination bracket out of the box Double elimination screenshot

I was scouring the world wide web for a good component library for visualizing single elimination brackets or double elimination brackets but most of them had complicated data structures or didn't allow for easy styling, and so I had to build my own, and decided to share it with the world.

Built With

You only need to have react installed in your project to use this project.

Note: default browser css is reset using the minireset.css package in the storybook demos. To fully control what the match component looks like you can build and supply your own view component for it

Getting Started

Installation

This project is hosted on the public npm registry, here's the link to the npm page

npm install @g-loot/react-tournament-brackets

Basic Usage

Basics of the library

import { SingleEliminationBracket, DoubleEliminationBracket, Match, MATCH_STATES, SVGViewer } from '@g-loot/react-tournament-brackets';

ComponentDescription
SingleEliminationBracketComponent for displaying single elimination bracket
DoubleEliminationBracketComponent for displaying double elimination bracket
MatchDefault component for rendering matches that can be overridden
MATCH_STATESConstant containing enum for Match states and Participants statuses
SVGViewerOptional component for displaying the bracket in a fixed size window with panning and zooming functionality

Using the components

This component generates an SVG of all your bracket matches, you can use the supplied optional component <SVGViewer /> like in the following example to wrap the SVG in a fixed size window with panning and zooming functionality, Note that you're also free to come up with your own solution for allowing the user to navigate giant brackets with ease.

import { SingleEliminationBracket, DoubleEliminationBracket, Match, SVGViewer } from '@g-loot/react-tournament-brackets';
export const DoubleElimination = () => (
  <DoubleEliminationBracket
    matches={matches}
    matchComponent={Match}
    svgWrapper={({ children, ...props }) => (
      <SVGViewer width={500} height={500} {...props}>
        {children}
      </SVGViewer>
    )}
  />
);
export const SingleElimination = () => (
  <SingleEliminationBracket
    matches={matches}
    matchComponent={Match}
    svgWrapper={({ children, ...props }) => (
      <SVGViewer width={500} height={500} {...props}>
        {children}
      </SVGViewer>
    )}
  />
);
  • If you want the SVGViewer to fit it's container you will need some sort of hook to achieve that, like useWindowSize(), useComponentSize or your own custom solution
import { DoubleEliminationBracket, Match, SVGViewer } from '@g-loot/react-tournament-brackets';

export const DoubleElimination = () => {
  const [width, height] = useWindowSize();
  const finalWidth = Math.max(width - 50, 500);
  const finalHeight = Math.max(height - 100, 500);
  return (
    <DoubleEliminationBracket
      matches={matches}
      matchComponent={Match}
      svgWrapper={({ children, ...props }) => (
        <SVGViewer width={finalWidth} height={finalHeight} {...props}>
          {children}
        </SVGViewer>
      )}
    />
  );
};

For more examples, please refer to the [Storybook][demo-url]

Data structures

  • Single Eliminations matches prop structure
[
  ...,
  {
    "id": 260005,
    "name": "Final - Match",
    "nextMatchId": null, // Id for the nextMatch in the bracket, if it's final match it must be null OR undefined
    "tournamentRoundText": "4", // Text for Round Header
    "startTime": "2021-05-30",
    "state": "DONE", // 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | 'DONE' | 'SCORE_DONE' Only needed to decide walkovers and if teamNames are TBD (to be decided)
    "participants": [
      {
        "id": "c016cb2a-fdd9-4c40-a81f-0cc6bdf4b9cc", // Unique identifier of any kind
        "resultText": "WON", // Any string works
        "isWinner": false,
        "status": null, // 'PLAYED' | 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | null
        "name": "giacomo123"
      },
      {
        "id": "9ea9ce1a-4794-4553-856c-9a3620c0531b",
        "resultText": null,
        "isWinner": true,
        "status": null, // 'PLAYED' | 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY'
        "name": "Ant"
      }
    ]
  }
  ...
]

  • Double Eliminations matches prop structure
{
  "upper": [
    ...,
    {
      "id": 260006, // Unique identifier of any kind
      "name": "Semi Final - Match",
      "nextMatchId": null,  // Id for the next match in upper bracket, if it's final match it must be null OR undefined
      "nextLooserMatchId": null,  // Id for the next match in lower bracket, if it's final match or a lower bracket match it must be null OR undefined
      "startTime": "2021-05-30",
      "state": "DONE", // 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | 'DONE' | 'SCORE_DONE' Only needed to decide walkovers and if teamNames are TBD (to be decided)
      "participants": [
        {
          "id": "c016cb2a-fdd9-4c40-a81f-0cc6bdf4b9cc", // Unique identifier of any kind
          "resultText": "WON", // Any string works
          "isWinner": false,
          "status": null, // 'PLAYED' | 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | null
          "name": "giacomo123"
        },
        {
          "id": "9ea9ce1a-4794-4553-856c-9a3620c0531b",
          "resultText": "LOST", // Any string works
          "isWinner": true,
          "status": null, // 'PLAYED' | 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY'
          "name": "Ant"
        }
      ]
    }
    ...
  ],
  "lower": [
    ...,
    {
      "id": 260005,
      "name": "Semi Final - Match",
      "nextMatchId": 260006,
      "nextLooserMatchId": null,
      "tournamentRoundText": "4",
      "startTime": "2021-05-30",
      "state": "DONE", // 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | 'DONE' | 'SCORE_DONE' Only needed to decide walkovers and if teamNames are TBD (to be decided)
      "participants": [
        {
          "id": "c016cb2a-fdd9-4c40-a81f-0cc6bdf4b9cc", // Unique identifier of any kind
          "resultText": "WON", // Any string works
          "isWinner": false,
          "status": null, // 'PLAYED' | 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | null
          "name": "giacomo123"
        },
        {
          "id": "9ea9ce1a-4794-4553-856c-9a3620c0531b",
          "resultText": null,
          "isWinner": true,
          "status": null, // 'PLAYED' | 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY'
          "name": "Ant"
        }
      ]
    }
    ...
  ]
}
{
  [upper|lower]: [
    ...,
    {
      "id": "WB R5 M1",
      "name": "WB R5 M1",
      "nextMatchId": "WB R6 M1",
      "nextLooserMatchId": "WB R6 M1",
      "startTime": null,
      "tournamentRound": "R5",
      "state": "SCORE_DONE",
      "participants": [
        {
          "id": "ddfee063-adde-4192-95d2-203eb2ebb8f7",
          "resultText": "",
          "isWinner": false,
          "status": "PLAYED",
          "name": "#1"
        }
      ]
    },
    {
      "id": "WB R6 M1",
      "name": "WB R6 M1",
      "nextMatchId": null,
      "nextLooserMatchId": null,
      "startTime": null,
      "tournamentRound": "R6",
      "state": "SCORE_DONE",
      "participants": []
    }
    ...,
  ]
}


  • Match / Participant States are defined in the exported constant MATCH_STATES
import { MATCH_STATES } from '@g-loot/react-tournament-brackets';
console.log(MATCH_STATES);
// {
//   PLAYED: 'PLAYED',
//   NO_SHOW: 'NO_SHOW',
//   WALK_OVER: 'WALK_OVER',
//   NO_PARTY: 'NO_PARTY',
//   DONE: 'DONE',
//   SCORE_DONE: 'SCORE_DONE',
// };

For more examples of accepted data, check out the mock data folder

Theming and Styling

This component's default theme is the dark theme in the screenshot, you can use the function createTheme which is exported from the library to create a theme and then pass it to either single or double bracket on the theme prop A few notes:

  • Some colors like the roundHeaders, and connectors aren't tied to the theme yet, you'll need to style those through the options prop manually for now, In the very near future they will be tied to the theme as well!

Full Example of custom theming:

import { SingleEliminationBracket, Match, SVGViewer, createTheme } from '@g-loot/react-tournament-brackets';

const WhiteTheme = createTheme({
  textColor: { main: '#000000', highlighted: '#07090D', dark: '#3E414D' },
  matchBackground: { wonColor: '#daebf9', lostColor: '#96c6da' },
  score: {
    background: { wonColor: '#87b2c4', lostColor: '#87b2c4' },
    text: { highlightedWonColor: '#7BF59D', highlightedLostColor: '#FB7E94' },
  },
  border: {
    color: '#CED1F2',
    highlightedColor: '#da96c6',
  },
  roundHeader: { backgroundColor: '#da96c6', fontColor: '#000' },
  connectorColor: '#CED1F2',
  connectorColorHighlight: '#da96c6',
  svgBackground: '#FAFAFA',
});

export const WhiteThemeBracket = () => {
  const [width, height] = useWindowSize();
  const finalWidth = Math.max(width - 50, 500);
  const finalHeight = Math.max(height - 100, 500);

  return (
    <SingleEliminationBracket
      matches={simpleSmallBracket}
      matchComponent={Match}
      theme={WhiteTheme}
      options={{
        style: {
          roundHeader: {
            backgroundColor: WhiteTheme.roundHeader.backgroundColor,
            fontColor: WhiteTheme.roundHeader.fontColor,
          },
          connectorColor: WhiteTheme.connectorColor,
          connectorColorHighlight: WhiteTheme.connectorColorHighlight,
        },
      }}
      svgWrapper={({ children, ...props }) => (
        <SvgViewer
          background={WhiteTheme.svgBackground}
          SVGBackground={WhiteTheme.svgBackground}
          width={finalWidth}
          height={finalHeight}
          {...props}
        >
          {children}
        </SvgViewer>
      )}
    />
  );
};

If themes aren't enough for you you can always supply your own Match view component and use the props passed to it to override the match view rendering, Again you'll need to style the round headers and connector lines using options prop

Basic example of custom match component

import { SingleEliminationBracket, SVGViewer } from '@g-loot/react-tournament-brackets';

export const CustomMatchBracket = () => {
  const [width, height] = useWindowSize();
  const finalWidth = Math.max(width - 50, 500);
  const finalHeight = Math.max(height - 100, 500);
  return (
    <SingleEliminationBracket
      matches={simpleSmallBracket}
      options={{
        style: {
          roundHeader: { backgroundColor: '#AAA' },
          connectorColor: '#FF8C00',
          connectorColorHighlight: '#000',
        },
      }}
      svgWrapper={({ children, ...props }) => (
        <SvgViewer
          background="#FFF"
          SVGBackground="#FFF"
          width={finalWidth}
          height={finalHeight}
          {...props}
        >
          {children}
        </SvgViewer>
      )}
      matchComponent={({
        match,
        onMatchClick,
        onPartyClick,
        onMouseEnter,
        onMouseLeave,
        topParty,
        bottomParty,
        topWon,
        bottomWon,
        topHovered,
        bottomHovered,
        topText,
        bottomText,
        connectorColor,
        computedStyles,
        teamNameFallback,
        resultFallback,
      }) => (
        <div
          style={{
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'space-around',
            color: '#000',
            width: '100%',
            height: '100%',
          }}
        >
          <div
            onMouseEnter={() => onMouseEnter(topParty.id)}
            style={{ display: 'flex' }}
          >
            <div>{topParty.name || teamNameFallback}</div>
            <div>{topParty.resultText ?? resultFallback(topParty)}</div>
          </div>
          <div
            style={{ height: '1px', width: '100%', background: '#FF8C00' }}
          />
          <div
            onMouseEnter={() => onMouseEnter(bottomParty.id)}
            style={{ display: 'flex' }}
          >
            <div>{bottomParty.name || teamNameFallback}</div>
            <div>{bottomParty.resultText ?? resultFallback(topParty)}</div>
          </div>
        </div>
      )}
    />
  );
};

For more examples, checkout the [live storybook][demo-url]

License

Distributed under the GNU LGPL v2.1 License. See LICENSE for more information.

Contributing

Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. For automatic feedback if you're editing the components or adding new ones run npm run dev
  4. Test your changes according to the Contributor Testing Guide
  5. Commit your Changes (git commit -m 'Add some AmazingFeature')
  6. Push to the Branch (git push origin feature/AmazingFeature)
  7. Open a Pull Request

Contributer Testing Guide

Unit tests and linting

  • Run: npm run test
  • Run: npm run lint

Checking that storybook works

  • Run: npm run build-storybook to check that it builds the storybook demo website correctly

Checking that the package works when consumed by a test project

  • Run: npm run test-lib-build for testing that the package builds and packs into an npm package correctly
  • Follow the guide for both nextjs and create-react-app project examples

Acknowledgements

  • Inspiration for allowing user to theme the bracket utlizing styled-components wthout exposing the library from React Data Table Component
  • Library used for the optional component SVGViewer for zooming and panning the bracket React SVG Pan Zoom

版本与发布记录

@g-loot/react-tournament-brackets 共发布 43 个版本,首次发布于 2021/06/23;过去 12 个月发布了 0 个版本。

版本发布日期距今
1.0.31-rclatest2023/12/042 年前
1.0.302023/06/113 年前
1.0.292023/06/113 年前
1.0.282023/03/053 年前
1.0.272023/02/273 年前
1.0.262023/02/273 年前

关于 @g-loot/react-tournament-brackets 的常见问题

@g-loot/react-tournament-brackets 是什么?

A react component to visualize bracket leaderboards

如何安装 @g-loot/react-tournament-brackets?

在项目目录运行 npm install @g-loot/react-tournament-brackets,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.1.0)、react-dom(^18.1.0)、react-svg-pan-zoom(^3.10.0)、styled-components(^4.1.3)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@g-loot/react-tournament-brackets 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@g-loot/react-tournament-brackets 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@g-loot/react-tournament-brackets 可以商用吗?

@g-loot/react-tournament-brackets 使用 ISC 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@g-loot/react-tournament-brackets 的下载量有多少?

根据 npm 官方下载统计,@g-loot/react-tournament-brackets 在 2026/10/16 – 2026/11/14 这 30 天内被下载 12,584 次,日均约 419 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@g-loot/react-tournament-brackets 还在维护吗?

最新版本 1.0.31-rc 发布于 2023/12/04(2 年前);至今共发布 43 个版本,其中过去 12 个月发布了 0 个;首次发布于 2021/06/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。