add mlb page
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
.thing
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
title: Balance Changes
|
|
||||||
description: Learn about the multiplayer balance changes in Balatro MP, including new jokers, planet cards, and adjustments to glass cards and tarot cards.
|
|
||||||
---
|
|
||||||
|
|
||||||
## Jokers
|
|
||||||
|
|
||||||
### Multiplayer Jokers
|
|
||||||
|
|
||||||
There are **10 added multiplayer jokers**. All jokers added in the multiplayer mod are designed
|
|
||||||
to **interact with your opponent** and will not show up in single player games.
|
|
||||||
See page 11 for a list of added jokers.
|
|
||||||
|
|
||||||
<Callout>
|
|
||||||
These jokers can be removed by unchecking the **"Enable Multiplayer Cards"** option in the lobby options.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
### Disabled Jokers
|
|
||||||
The following jokers are **unavailable** in multiplayer due to their interaction with boss blinds:
|
|
||||||
- **Chicot**
|
|
||||||
- **Matador**
|
|
||||||
- **Mr. Bones**
|
|
||||||
- **Luchador**
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
#### Hanging Chad
|
|
||||||
Hanging Chad has been reworked to **retrigger the first 2 cards once** instead of the first card twice.
|
|
||||||
|
|
||||||
## Planet cards
|
|
||||||
|
|
||||||
|
|
||||||
### Asteroid
|
|
||||||
|
|
||||||
**Removes one level** from your opponent's highest levelled planet.
|
|
||||||
|
|
||||||
<Callout>
|
|
||||||
These planet cards can be removed by unchecking the **"Enable Multiplayer Cards"** option in the lobby options.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Tarot Cards
|
|
||||||
|
|
||||||
### Justice
|
|
||||||
|
|
||||||
This card is not available in the standard ruleset.
|
|
||||||
|
|
||||||
## Enhancements
|
|
||||||
### Glass Cards
|
|
||||||
|
|
||||||
Glass cards are now **1.5x** instead of 2x multiplier and can **only** be found in:
|
|
||||||
|
|
||||||
- Standard packs
|
|
||||||
- Spectral cards that spawn cards (Grim, Incantation, and Familiar)
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"index",
|
"index",
|
||||||
"getting-started",
|
"getting-started",
|
||||||
|
"multiplayer-content",
|
||||||
"rulesets",
|
"rulesets",
|
||||||
"ranked-matchmaking",
|
"ranked-matchmaking",
|
||||||
"advanced"
|
"advanced"
|
||||||
|
|||||||
5
content/docs/multiplayer-content/jokers.mdx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Jokers
|
||||||
|
description: Multiplayer-specific Jokers
|
||||||
|
---
|
||||||
|
|
||||||
4
content/docs/multiplayer-content/meta.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"pages": ["jokers"],
|
||||||
|
"defaultOpen": true
|
||||||
|
}
|
||||||
7
content/docs/ranked-matchmaking/faq.mdx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: FAQ
|
||||||
|
description: Answers to common questions about the Ranked Matchmaking.
|
||||||
|
---
|
||||||
|
|
||||||
|
## what happens if you take a rare skip? does your opponent get that rare in the next shop?
|
||||||
|
|
||||||
15
content/docs/ranked-matchmaking/introduction.mdx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: Introduction
|
||||||
|
description: Learn how to play ranked matches in Balatro Multiplayer.
|
||||||
|
---
|
||||||
|
|
||||||
|
If you want to compete in ranked matches in Balatro Multiplayer and make your way to the leaderboards you have two options:
|
||||||
|
|
||||||
|
## Ranked Queue
|
||||||
|
|
||||||
|
The **Ranked Queue** is the default queue in Balatro Multiplayer. It is a queue where players can compete against other players in a ranked matchmaking system.
|
||||||
|
|
||||||
|
One of the main differentiators of the Ranked Queue is that
|
||||||
|
**it includes changes to the base game balance.**
|
||||||
|
These changes are meant to make the game **less RNG-heavy and more balanced** for competitive play.
|
||||||
|
|
||||||
5
content/docs/ranked-matchmaking/meta.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"title": "Ranked Matchmaking",
|
||||||
|
"pages": ["introduction", "multiplayer-balance-changes", "faq"],
|
||||||
|
"defaultOpen": true
|
||||||
|
}
|
||||||
281
content/docs/ranked-matchmaking/multiplayer-balance-changes.mdx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
---
|
||||||
|
title: Balance Changes
|
||||||
|
description: Learn about the multiplayer balance changes in Balatro MP, including new jokers, planet cards, and adjustments to glass cards and tarot cards.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Jokers
|
||||||
|
|
||||||
|
### Multiplayer Jokers
|
||||||
|
|
||||||
|
There are **10 added multiplayer jokers**. All jokers added in the multiplayer mod are designed
|
||||||
|
to **interact with your opponent** and will not show up in single player games.
|
||||||
|
|
||||||
|
<div className={'relative overflow-auto'}>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={'text-center'}>Joker</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Defensive Joker [toc]
|
||||||
|
<JokerCard name={'Defensive Joker'} img={'/cards/j_defensive_joker.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Chips>+125</Chips> Chips for every life less than your Nemesis.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
There is a bug where this joker might appear in single player mode. To fix this, disable the multiplayer
|
||||||
|
mod when playing single player.
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Skip-Off [toc]
|
||||||
|
<JokerCard name={'Skip-Off'} img={'/cards/j_skip_off.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Hands>+1</Hands> Hands and <Hands>+1</Hands> Discards per additional blind skipped compared to your
|
||||||
|
Nemesis.
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Let's Go Gambling [toc]
|
||||||
|
<JokerCard name={'Let\'s Go Gambling'} img={'/cards/j_lets_go_gambling.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className={'flex flex-col gap-1'}>
|
||||||
|
<span><Chance>1 in 4</Chance> chance for <Xmult>X4</Xmult> Mult and <Money>$10</Money></span>
|
||||||
|
<span><Chance>1 in 8</Chance> chance to give your Nemesis <Money>$5</Money></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
The effect applies on each hand played.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Speedrun [toc]
|
||||||
|
<JokerCard name={'Speedrun'} img={'/cards/j_speedrun.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
If you reach a <Hands>PvP Blind</Hands> before your Nemesis, create a
|
||||||
|
random <Spectral>Spectral</Spectral> card. Must have room.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
There is a bug where the spectral card might not be created, but is is very rare. We're working on a
|
||||||
|
fix.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Conjoined Joker [toc]
|
||||||
|
<JokerCard name={'Conjoined Joker'} img={'/cards/j_conjoined_joker.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
While in a <Hands>PvP Blind</Hands>, gain
|
||||||
|
<Xmult>X1</Xmult> Mult for every <Hands>Hand</Hands>
|
||||||
|
your Nemesis has left
|
||||||
|
(Max <Xmult>X5</Xmult> Mult)
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Penny Pincher [toc]
|
||||||
|
<JokerCard name={'Penny Pincher'} img={'/cards/j_penny_pincher.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
At start of shop, gain
|
||||||
|
<Money>$1</Money> for every <Money>$3</Money> your Nemesis spent last shop
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Taxes [toc]
|
||||||
|
<JokerCard name={'Taxes'} img={'/cards/j_taxes.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
This Joker gains <Mult>+5</Mult> Mult each time your Nemesis sells a card
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Magnet [toc]
|
||||||
|
<JokerCard name={'Magnet'} img={'/cards/j_magnet.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
After <Hands>2</Hands> rounds, sell this card to Copy your Nemesis' highest sell cost Joker. Does not
|
||||||
|
copy Joker state.
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
If your Nemesis has multiple Jokers that have the highest sell cost, a random one will be copied.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Pizza [toc]
|
||||||
|
<JokerCard name={'Pizza'} img={'/cards/j_pizza.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className={'flex flex-col gap-1'}>
|
||||||
|
<span><Mult>+6</Mult> Discards for all players</span>
|
||||||
|
<span><Mult>-1</Mult> Discard when any player selects a blind</span>
|
||||||
|
<span>Eaten when your Nemesis skips</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Pacifist [toc]
|
||||||
|
<JokerCard name={'Pacifist'} img={'/cards/j_pacifist.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className={'flex flex-col gap-1'}>
|
||||||
|
<Xmult>X10</Xmult> Mult while not in a <Hands>PvP Blind</Hands>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Callout>
|
||||||
|
These jokers can be removed by unchecking the **"Enable Multiplayer Cards"** option in the lobby options.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
### Disabled Jokers
|
||||||
|
The following jokers are **unavailable** in multiplayer due to their interaction with boss blinds:
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={'text-center'}>Joker</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Chicot [toc]
|
||||||
|
<JokerCard name={'Chicot'} img={'/cards/j_chicot.png'} h={4}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Disables effect of every <Hands>Boss Blind</Hands>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Matador [toc]
|
||||||
|
<JokerCard name={'Matador'} img={'/cards/j_matador.png'} h={4}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Earn <Money>$8</Money> if played hand triggers the <Hands>Boss Blind</Hands> ability
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Mr. Bones [toc]
|
||||||
|
<JokerCard name={'Mr. Bones'} img={'/cards/j_mr_bones.png'} h={4}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Prevents Death if chips scored are at least <Hands>25%</Hands> of required chips.
|
||||||
|
<Mult>Self destructs</Mult>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Luchador [toc]
|
||||||
|
<JokerCard name={'Luchador'} img={'/cards/j_luchador.png'} h={4}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Sell this card to disable the current <Hands>Boss Blind</Hands>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
### Modified Jokers
|
||||||
|
|
||||||
|
These jokers are modified versions of the original jokers.
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={'text-center'}>Joker</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#### Hanging Chad [toc]
|
||||||
|
<JokerCard name={'Hanging Chad'} img={'/cards/j_hanging_chad.png'} h={4}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Retrigger <Hands>first</Hands> and <Hands>second</Hands> played cards used in
|
||||||
|
scoring <Hands>1</Hands> additional time
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Planet cards
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={'text-center'}>Joker</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
### Asteroid [toc]
|
||||||
|
<JokerCard name={'Asteroid'} img={'/cards/c_asteroid.png'}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Removes <Hands>one level</Hands> from your Nemesis' highest level <Hands>poker hand</Hands>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Callout>
|
||||||
|
These planet cards can be removed by unchecking the **"Enable Multiplayer Cards"** option in the lobby options.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Tarot Cards
|
||||||
|
|
||||||
|
### Justice
|
||||||
|
|
||||||
|
This card is not available in the standard ruleset.
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
### Glass Cards
|
||||||
|
|
||||||
|
Glass cards are now **1.5x** instead of 2x multiplier and can **only** be found in:
|
||||||
|
|
||||||
|
- Standard packs
|
||||||
|
- Spectral cards that spawn cards (Grim, Incantation, and Familiar)
|
||||||
BIN
public/images/players/bear.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/images/players/doc.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
public/images/players/edzy.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/images/players/gothic.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
public/images/players/haelian.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
public/images/players/malf.png
Normal file
|
After Width: | Height: | Size: 686 KiB |
BIN
public/images/players/nandre.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/images/players/neato.jpg
Normal file
|
After Width: | Height: | Size: 704 KiB |
BIN
public/images/players/roffle.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
public/images/players/seadubbs.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/images/players/skoottie.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/images/players/zaino.png
Normal file
|
After Width: | Height: | Size: 6.4 MiB |
@@ -0,0 +1,55 @@
|
|||||||
|
import { PlayerCard } from '@/app/(home)/major-league-balatro/_components/player-card'
|
||||||
|
import { players } from '@/app/(home)/major-league-balatro/_constants/players'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
|
||||||
|
export function Competitors() {
|
||||||
|
return (
|
||||||
|
<section className='container py-8 md:py-12'>
|
||||||
|
<div className='mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center'>
|
||||||
|
<h2 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
|
||||||
|
The Competitors
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-[85%] text-muted-foreground leading-normal sm:text-lg sm:leading-7'>
|
||||||
|
12 talented creators split into Blue and Red divisions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue='blue' className='mx-auto mt-8 max-w-5xl'>
|
||||||
|
<TabsList className='grid w-full grid-cols-2'>
|
||||||
|
<TabsTrigger value='blue'>Blue Division</TabsTrigger>
|
||||||
|
<TabsTrigger value='red'>Red Division</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value='blue' className='mt-6'>
|
||||||
|
<div className='grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3'>
|
||||||
|
{Object.values(players)
|
||||||
|
.filter((player) => player.division === 'Blue')
|
||||||
|
.map((creator) => (
|
||||||
|
<PlayerCard
|
||||||
|
name={creator.name}
|
||||||
|
socials={creator.socials}
|
||||||
|
picture={creator.picture}
|
||||||
|
key={creator.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='red' className='mt-6'>
|
||||||
|
<div className='grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3'>
|
||||||
|
{Object.values(players)
|
||||||
|
.filter(
|
||||||
|
(player) => player.division === 'Red' && player.id !== 'tbd'
|
||||||
|
)
|
||||||
|
.map((creator) => (
|
||||||
|
<PlayerCard
|
||||||
|
name={creator.name}
|
||||||
|
socials={creator.socials}
|
||||||
|
picture={creator.picture}
|
||||||
|
key={creator.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
src/app/(home)/major-league-balatro/_components/hero.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Calendar, ChevronRight } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export function HeroSection() {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className='container relative z-10 flex flex-col items-center py-16 text-center md:py-24'>
|
||||||
|
<Badge className='mb-4 w-full bg-red-600 text-sm text-white hover:bg-red-700 sm:w-auto'>
|
||||||
|
Season 1
|
||||||
|
</Badge>
|
||||||
|
<h1 className='mb-4 font-extrabold text-4xl tracking-tight md:text-6xl'>
|
||||||
|
Major League Balatro
|
||||||
|
</h1>
|
||||||
|
<p className='mb-8 max-w-[42rem] text-lg text-muted-foreground md:text-xl'>
|
||||||
|
12 creators. 2 divisions. 1 champion. The ultimate Balatro multiplayer
|
||||||
|
tournament.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className='mb-8 grid w-full max-w-3xl grid-cols-2 gap-4 md:grid-cols-4 md:gap-8'>
|
||||||
|
<div className='flex flex-col items-center rounded-lg border bg-card p-4'>
|
||||||
|
<span className='font-bold text-2xl text-red-500 md:text-3xl'>
|
||||||
|
12
|
||||||
|
</span>
|
||||||
|
<span className='text-muted-foreground text-sm'>Creators</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col items-center rounded-lg border bg-card p-4'>
|
||||||
|
<span className='font-bold text-2xl text-red-500 md:text-3xl'>
|
||||||
|
5
|
||||||
|
</span>
|
||||||
|
<span className='text-muted-foreground text-sm'>Weeks</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col items-center rounded-lg border bg-card p-4'>
|
||||||
|
<span className='font-bold text-2xl text-red-500 md:text-3xl'>
|
||||||
|
$500+
|
||||||
|
</span>
|
||||||
|
<span className='text-muted-foreground text-sm'>Prize Pool</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col items-center rounded-lg border bg-card p-4'>
|
||||||
|
<span className='font-bold text-2xl text-red-500 md:text-3xl'>
|
||||||
|
May 17
|
||||||
|
</span>
|
||||||
|
<span className='text-muted-foreground text-sm'>Finals</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 sm:flex-row'>
|
||||||
|
<Link href='#schedule'>
|
||||||
|
<Button
|
||||||
|
size='lg'
|
||||||
|
className='bg-red-600 text-white hover:bg-red-700'
|
||||||
|
>
|
||||||
|
View Schedule
|
||||||
|
<Calendar className='ml-2 h-4 w-4' />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href='#format'>
|
||||||
|
<Button variant='outline' size='lg'>
|
||||||
|
Tournament Format
|
||||||
|
<ChevronRight className='ml-2 h-4 w-4' />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { SiTwitch } from '@icons-pack/react-simple-icons'
|
||||||
|
import { Twitch } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import type { Player } from '../types'
|
||||||
|
import { PlayerAvatar } from './player-avatar'
|
||||||
|
|
||||||
|
type NextMatchInfoCardProps = {
|
||||||
|
player1: Player
|
||||||
|
player2: Player
|
||||||
|
week: number | string
|
||||||
|
bestOf: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NextMatchInfoCard({
|
||||||
|
player1,
|
||||||
|
player2,
|
||||||
|
week,
|
||||||
|
bestOf,
|
||||||
|
}: NextMatchInfoCardProps) {
|
||||||
|
return (
|
||||||
|
<div className='mt-10 overflow-hidden rounded-xl border bg-card/60 shadow-lg backdrop-blur-sm'>
|
||||||
|
<div className='flex items-center justify-between gap-4 px-6 py-4'>
|
||||||
|
<div className={'flex flex-col gap-1'}>
|
||||||
|
<div className='text-center text-muted-foreground text-sm'>
|
||||||
|
{typeof week === 'string' ? week : `Week ${week}`}
|
||||||
|
</div>
|
||||||
|
<div className={'font-bold text-muted-foreground text-sm'}>
|
||||||
|
Best of {bestOf}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className='flex items-center gap-2 font-bold text-xl md:text-2xl'>
|
||||||
|
<div className={'flex items-center gap-2'}>
|
||||||
|
<PlayerAvatar
|
||||||
|
className={'size-16'}
|
||||||
|
playerName={player1.name}
|
||||||
|
img={player1.picture}
|
||||||
|
/>
|
||||||
|
{player1.name}
|
||||||
|
</div>
|
||||||
|
<span className='text-red-500'>vs</span>
|
||||||
|
<div className={'flex items-center gap-2'}>
|
||||||
|
{player2.name}
|
||||||
|
<PlayerAvatar
|
||||||
|
className={'size-16'}
|
||||||
|
playerName={player2.name}
|
||||||
|
img={player2.picture}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button className='w-full gap-2 bg-red-600 text-white hover:bg-red-700 md:w-auto'>
|
||||||
|
<SiTwitch className='h-4 w-4' />
|
||||||
|
Watch Live
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={`https://twitch.tv/${player1.socials.twitch}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{player1.name}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={`https://twitch.tv/${player2.socials.twitch}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{player2.name}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { NextMatchInfoCard } from '@/app/(home)/major-league-balatro/_components/next-match-info-card'
|
||||||
|
import { CountdownTimer } from '@/components/countdown-timer'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Calendar, Clock } from 'lucide-react'
|
||||||
|
import type { PropsWithChildren } from 'react'
|
||||||
|
import { players } from '../_constants/players'
|
||||||
|
import type { Match } from '../types'
|
||||||
|
|
||||||
|
export type NextMatchInfoProps = {
|
||||||
|
nextMatch: Match | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NextMatchInfo({ nextMatch }: NextMatchInfoProps) {
|
||||||
|
if (!nextMatch) {
|
||||||
|
return (
|
||||||
|
<SectionContainer>
|
||||||
|
<Card className='border-2 border-red-500/50'>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className='text-center text-2xl'>
|
||||||
|
Tournament Schedule
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='py-8 text-center'>
|
||||||
|
<div className='mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted'>
|
||||||
|
<Calendar className='h-8 w-8 text-muted-foreground' />
|
||||||
|
</div>
|
||||||
|
<p className='text-muted-foreground'>
|
||||||
|
No upcoming matches scheduled at this time.
|
||||||
|
</p>
|
||||||
|
<p className='mt-2 text-muted-foreground text-sm'>
|
||||||
|
Check back later for updates or view past matches below.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</SectionContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMatchPlayer1 = players[nextMatch.player1Id]
|
||||||
|
const nextMatchPlayer2 = players[nextMatch.player2Id]
|
||||||
|
|
||||||
|
if (!nextMatchPlayer1) {
|
||||||
|
throw new Error(`Player ${nextMatch.player1Id} not found`)
|
||||||
|
}
|
||||||
|
if (!nextMatchPlayer2) {
|
||||||
|
throw new Error(`Player ${nextMatch.player2Id} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContainer>
|
||||||
|
<div className='relative overflow-hidden rounded-xl border-2 bg-gradient-to-b from-background '>
|
||||||
|
<div className='relative z-10 px-6 py-8 md:py-10'>
|
||||||
|
<div className='mb-8 text-center'>
|
||||||
|
<h2 className='mb-2 font-bold text-2xl md:text-3xl'>
|
||||||
|
Next Match Countdown
|
||||||
|
</h2>
|
||||||
|
<div className='flex items-center justify-center gap-2 text-red-500'>
|
||||||
|
<Clock className='h-5 w-5 animate-pulse' />
|
||||||
|
<p className='text-sm md:text-base'>
|
||||||
|
{nextMatch.date} - {nextMatch.time}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CountdownTimer nextMatch={nextMatch} />
|
||||||
|
|
||||||
|
<NextMatchInfoCard
|
||||||
|
week={nextMatch.week}
|
||||||
|
bestOf={5}
|
||||||
|
player1={nextMatchPlayer1}
|
||||||
|
player2={nextMatchPlayer2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionContainer({ children }: PropsWithChildren) {
|
||||||
|
return (
|
||||||
|
<section className='container py-8 md:py-12'>
|
||||||
|
<div className='mx-auto w-full max-w-4xl'>{children}</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ExternalLink, Twitch, Users, Youtube } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export function Organizer() {
|
||||||
|
return (
|
||||||
|
<section className='container py-8 md:py-12'>
|
||||||
|
<div className='mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center'>
|
||||||
|
<h2 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
|
||||||
|
About the Organizer
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mx-auto mt-8 flex max-w-3xl flex-col items-center gap-8 md:flex-row'>
|
||||||
|
<div className='relative h-32 w-32 flex-shrink-0 rounded-full bg-muted'>
|
||||||
|
<div className='absolute inset-0 flex items-center justify-center'>
|
||||||
|
<Users className='h-16 w-16 text-muted-foreground/50' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className='mb-2 font-bold text-2xl'>ZainoTV</h3>
|
||||||
|
<p className='mb-4 text-muted-foreground'>
|
||||||
|
ZainoTV is the creator and organizer of Major League Balatro. With a
|
||||||
|
passion for competitive gaming and the Balatro community, Zaino has
|
||||||
|
brought together 12 talented creators for this exciting tournament.
|
||||||
|
</p>
|
||||||
|
<div className='flex gap-4'>
|
||||||
|
<Link
|
||||||
|
href='https://twitch.tv/zainotv'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<Button variant='outline' size='sm' className='gap-2'>
|
||||||
|
<Twitch className='h-4 w-4' />
|
||||||
|
Twitch
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href='https://www.youtube.com/@ZainoTVLive'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<Button variant='outline' size='sm' className='gap-2'>
|
||||||
|
<Youtube className='h-4 w-4' />
|
||||||
|
YouTube
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
|
interface PlayerAvatarProps {
|
||||||
|
playerName: string
|
||||||
|
img?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlayerAvatar({
|
||||||
|
playerName,
|
||||||
|
img,
|
||||||
|
className,
|
||||||
|
}: PlayerAvatarProps) {
|
||||||
|
const initials = getInitials(playerName)
|
||||||
|
return (
|
||||||
|
<Avatar className={className}>
|
||||||
|
<AvatarImage src={img} alt={playerName} />
|
||||||
|
<AvatarFallback>{initials}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitials(name: string) {
|
||||||
|
if (!name || name === 'TBD') return '?'
|
||||||
|
|
||||||
|
const parts = name.split(' ')
|
||||||
|
if (parts.length === 1) return name.substring(0, 2).toUpperCase()
|
||||||
|
return (
|
||||||
|
(parts?.[0]?.[0] ?? '') + (parts[parts.length - 1]?.[0] ?? '')
|
||||||
|
).toUpperCase()
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Player } from '@/app/(home)/major-league-balatro/types'
|
||||||
|
import { OptimizedImage } from '@/components/optimized-image'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
|
import { SiTwitch, SiYoutube } from '@icons-pack/react-simple-icons'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export type PlayerCardProps = {
|
||||||
|
picture: string
|
||||||
|
name: string
|
||||||
|
socials: Player['socials']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlayerCard(player: PlayerCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className='overflow-hidden py-0'>
|
||||||
|
<div className='relative aspect-square bg-muted'>
|
||||||
|
<div className='absolute inset-0 flex items-center justify-center'>
|
||||||
|
<OptimizedImage
|
||||||
|
src={player.picture}
|
||||||
|
alt={player.name}
|
||||||
|
className={'h-full w-full object-cover'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardContent className='p-4'>
|
||||||
|
<h3 className='font-bold text-lg'>{player.name}</h3>
|
||||||
|
<div className={'flex items-center justify-between gap-1'}>
|
||||||
|
<div>@{player.socials.twitch || player.socials.youtube || ''}</div>
|
||||||
|
<div className={'flex items-center gap-3'}>
|
||||||
|
{player.socials.twitch && (
|
||||||
|
<Link
|
||||||
|
href={`https://twitch.tv/${player.socials.twitch}`}
|
||||||
|
target={'_blank'}
|
||||||
|
rel={'noopener noreferrer'}
|
||||||
|
>
|
||||||
|
<SiTwitch className={'size-4'} />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{player.socials.youtube && (
|
||||||
|
<Link
|
||||||
|
href={`https://youtube.com/@${player.socials.youtube}`}
|
||||||
|
target={'_blank'}
|
||||||
|
rel={'noopener noreferrer'}
|
||||||
|
>
|
||||||
|
<SiYoutube className={'size-4'} />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Heart } from 'lucide-react'
|
||||||
|
|
||||||
|
export function PrizePool() {
|
||||||
|
return (
|
||||||
|
<section className='container py-8 md:py-12'>
|
||||||
|
<div className='mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center'>
|
||||||
|
<h2 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
|
||||||
|
Prize Pool
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-[85%] text-muted-foreground leading-normal sm:text-lg sm:leading-7'>
|
||||||
|
Playing for charity and glory.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mx-auto mt-8 max-w-3xl'>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className='flex items-center justify-center gap-2'>
|
||||||
|
<Heart className='h-5 w-5 text-red-500' />
|
||||||
|
Charity Prize Pool
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='text-center'>
|
||||||
|
<div className='mb-4 font-bold text-5xl'>
|
||||||
|
$500<span className='text-red-500'>+</span>
|
||||||
|
</div>
|
||||||
|
<p className='mb-6 text-muted-foreground'>
|
||||||
|
The prize pool is creator-funded and will be donated to the
|
||||||
|
winner's charity of choice. We're hoping to add to this amount as
|
||||||
|
the tournament progresses.
|
||||||
|
</p>
|
||||||
|
<Separator className='my-6' />
|
||||||
|
<div className='text-muted-foreground text-sm'>
|
||||||
|
Each creator is playing for a charity of their choice. The full
|
||||||
|
amount will be donated to the champion's selected charity at the
|
||||||
|
conclusion of the tournament.
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
386
src/app/(home)/major-league-balatro/_components/schedule.tsx
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
import { players } from '@/app/(home)/major-league-balatro/_constants/players'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card } from '@/components/ui/card'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { SiTwitch, SiYoutube } from '@icons-pack/react-simple-icons'
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
Camera,
|
||||||
|
Clock,
|
||||||
|
TvMinimalPlay,
|
||||||
|
Twitch,
|
||||||
|
Youtube,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import type { BadgeProps, Match, WeekConfig } from '../types'
|
||||||
|
import { PlayerAvatar } from './player-avatar'
|
||||||
|
|
||||||
|
const WEEK_CONFIG: Record<string | number, WeekConfig> = {
|
||||||
|
1: {
|
||||||
|
label: 'Week 1: April 6-12, 2025',
|
||||||
|
badgeProps: {
|
||||||
|
variant: 'outline',
|
||||||
|
className: 'border-green-500 bg-green-500/10 text-green-500',
|
||||||
|
text: 'Completed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
label: 'Week 2: April 13-19, 2025',
|
||||||
|
badgeProps: {
|
||||||
|
variant: 'outline',
|
||||||
|
className: 'border-blue-500 bg-blue-500/10 text-blue-500',
|
||||||
|
text: 'Current Week',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
label: 'Week 3: April 20-26, 2025',
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
label: 'Week 4: April 27-May 3, 2025',
|
||||||
|
},
|
||||||
|
// 5: {
|
||||||
|
// label: 'Week 5: May 4-10, 2025',
|
||||||
|
// },
|
||||||
|
// 'Play-in': {
|
||||||
|
// label: 'Play-in Week: May 11-16, 2025',
|
||||||
|
// status: 'playoff',
|
||||||
|
// badgeProps: {
|
||||||
|
// variant: 'outline',
|
||||||
|
// className: 'border-purple-500 bg-purple-500/10 text-purple-500',
|
||||||
|
// text: 'Playoff',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// Finals: {
|
||||||
|
// label: 'League Finals: May 17, 2025',
|
||||||
|
// status: 'finals',
|
||||||
|
// badgeProps: {
|
||||||
|
// variant: 'outline',
|
||||||
|
// className: 'border-red-500 bg-red-500/10 text-red-500',
|
||||||
|
// text: 'Championship',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_BADGE_PROPS: BadgeProps = {
|
||||||
|
variant: 'outline',
|
||||||
|
className: 'bg-muted text-muted-foreground',
|
||||||
|
text: 'Upcoming',
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusBadgeProps = {
|
||||||
|
status: 'completed' | 'current' | 'upcoming'
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusBadge = ({ status }: StatusBadgeProps) => {
|
||||||
|
const { variant, className } =
|
||||||
|
WEEK_CONFIG[status]?.badgeProps || DEFAULT_BADGE_PROPS
|
||||||
|
|
||||||
|
const text =
|
||||||
|
status === 'completed'
|
||||||
|
? 'Completed'
|
||||||
|
: status === 'current'
|
||||||
|
? 'Current Week'
|
||||||
|
: 'Upcoming'
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant={variant}
|
||||||
|
className={cn(
|
||||||
|
status === 'completed'
|
||||||
|
? 'border-green-500 bg-green-500/10 text-green-500'
|
||||||
|
: status === 'current'
|
||||||
|
? 'border-blue-500 bg-blue-500/10 text-blue-500'
|
||||||
|
: 'bg-muted text-muted-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatchDivisionProps = {
|
||||||
|
division: 'Blue' | 'Red'
|
||||||
|
}
|
||||||
|
|
||||||
|
const MatchDivision = ({ division }: MatchDivisionProps) => {
|
||||||
|
const bgColor = division === 'Blue' ? 'bg-blue-950/20' : 'bg-red-950/20'
|
||||||
|
const textColor =
|
||||||
|
division === 'Blue'
|
||||||
|
? 'border-blue-500 text-blue-500'
|
||||||
|
: 'border-red-500 text-red-500'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-center p-4 sm:w-1/4 ${bgColor}`}>
|
||||||
|
<Badge variant='outline' className={textColor}>
|
||||||
|
{division} Division
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatchDateTimeProps = {
|
||||||
|
date: string
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MatchDateTime = ({ date, time }: MatchDateTimeProps) => (
|
||||||
|
<div className='flex items-center gap-2 text-muted-foreground text-sm'>
|
||||||
|
<Calendar className='h-4 w-4' />
|
||||||
|
<span>{date}</span>
|
||||||
|
<Clock className='ml-2 h-4 w-4' />
|
||||||
|
<span>{time}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
type VodButtonProps = {
|
||||||
|
url: string
|
||||||
|
player: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const VodButton = ({ url, player }: VodButtonProps) => (
|
||||||
|
<Link href={url} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<Button variant='outline' size='sm' className='gap-1'>
|
||||||
|
<Youtube className='h-4 w-4' />
|
||||||
|
{player}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
type LiveButtonProps = {
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const LiveButton = ({ username }: LiveButtonProps) => (
|
||||||
|
<Link
|
||||||
|
href={`https://twitch.tv/${username.toLowerCase()}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<Button variant='outline' size='sm' className='gap-1'>
|
||||||
|
<Twitch className='h-4 w-4' />
|
||||||
|
Watch Live
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
type MatchCardProps = {
|
||||||
|
match: Match
|
||||||
|
}
|
||||||
|
|
||||||
|
const MatchCard = ({ match }: MatchCardProps) => {
|
||||||
|
const { player1Id, player2Id, date, time, completed, vod1, vod2 } = match
|
||||||
|
|
||||||
|
const player1 = players[player1Id]
|
||||||
|
const player2 = players[player2Id]
|
||||||
|
|
||||||
|
if (!player1) {
|
||||||
|
throw new Error(`Player ${player1Id} not found`)
|
||||||
|
}
|
||||||
|
if (!player2) {
|
||||||
|
throw new Error(`Player ${player2Id} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className='overflow-hidden'>
|
||||||
|
<div className='flex flex-col sm:flex-row'>
|
||||||
|
<div className='flex items-center justify-center p-4 sm:w-1/4'>
|
||||||
|
<div className='text-muted-foreground text-sm'>
|
||||||
|
{date} • {time}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='p-4 sm:w-3/4'>
|
||||||
|
<div className='flex flex-col justify-between gap-4 sm:flex-row sm:items-center'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<div className='-space-x-4 flex'>
|
||||||
|
<PlayerAvatar playerName={player1.name} img={player1.picture} />
|
||||||
|
<PlayerAvatar playerName={player2.name} img={player2.picture} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className='font-bold text-lg'>
|
||||||
|
{player1.name} vs {player2.name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
{completed ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant='outline' size='sm' className='gap-1'>
|
||||||
|
<TvMinimalPlay className='size-4' />
|
||||||
|
Watch
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
{vod1 && (
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={vod1}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<SiYoutube className='h-4 w-4' />
|
||||||
|
{player1.name}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{vod2 && (
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={vod2}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<SiYoutube className='h-4 w-4' />
|
||||||
|
{player2.name}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant='outline' size='sm' className='gap-1'>
|
||||||
|
<SiTwitch className='size-4' />
|
||||||
|
Watch Live
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={`https://twitch.tv/${player1.socials.twitch}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{player1.name}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={`https://twitch.tv/${player2.socials.twitch}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{player2.name}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeekTabProps = {
|
||||||
|
week: string | number
|
||||||
|
matches: Match[]
|
||||||
|
status: 'current' | 'completed' | 'upcoming'
|
||||||
|
}
|
||||||
|
|
||||||
|
const WeekTab = ({ week, matches, status }: WeekTabProps) => {
|
||||||
|
const weekConfig = WEEK_CONFIG[week]
|
||||||
|
if (!weekConfig) {
|
||||||
|
throw new Error(`Week ${week} not found in WEEK_CONFIG`)
|
||||||
|
}
|
||||||
|
const filteredMatches = useMemo(
|
||||||
|
() =>
|
||||||
|
matches.filter(
|
||||||
|
(m) => m.week === week || m.week === Number.parseInt(String(week))
|
||||||
|
),
|
||||||
|
[matches, week]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContent value={`week${week}`} className='space-y-6'>
|
||||||
|
<div className='mb-4 flex items-center justify-between'>
|
||||||
|
<h3 className='font-bold text-xl'>{weekConfig.label}</h3>
|
||||||
|
<StatusBadge status={status} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredMatches.map((match, index) => (
|
||||||
|
<MatchCard key={index} match={match} />
|
||||||
|
))}
|
||||||
|
</TabsContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type MlbScheduleProps = {
|
||||||
|
matches: Match[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MlbSchedule({ matches }: MlbScheduleProps) {
|
||||||
|
const sortedMatches = useMemo(
|
||||||
|
() =>
|
||||||
|
[...matches].sort((a, b) => {
|
||||||
|
if (typeof a.week === 'string' || typeof b.week === 'string') {
|
||||||
|
return String(a.week).localeCompare(String(b.week))
|
||||||
|
}
|
||||||
|
return a.week - b.week
|
||||||
|
}),
|
||||||
|
[matches]
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentWeek = useMemo(() => {
|
||||||
|
const now = new Date()
|
||||||
|
return (
|
||||||
|
sortedMatches.find((match) => new Date(match.datetime) > now)?.week || 1
|
||||||
|
)
|
||||||
|
}, [sortedMatches])
|
||||||
|
console.log(currentWeek)
|
||||||
|
return (
|
||||||
|
<section id='schedule' className='container py-8 md:py-12'>
|
||||||
|
<div className='mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center'>
|
||||||
|
<h2 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
|
||||||
|
Tournament Schedule
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-[85%] text-muted-foreground leading-normal sm:text-lg sm:leading-7'>
|
||||||
|
All matches from April to May 2025
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mx-auto mt-8 max-w-5xl'>
|
||||||
|
<Tabs defaultValue={`week${currentWeek}`}>
|
||||||
|
<TabsList className='mb-4 flex flex-wrap justify-center'>
|
||||||
|
{Object.keys(WEEK_CONFIG).map((week) => (
|
||||||
|
<TabsTrigger key={week} value={`week${week}`}>
|
||||||
|
Week {week}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{Object.keys(WEEK_CONFIG).map((week) => {
|
||||||
|
const weekNumber = Number.parseInt(week)
|
||||||
|
const status =
|
||||||
|
!Number.isNaN(weekNumber) && weekNumber === currentWeek
|
||||||
|
? 'current'
|
||||||
|
: // @ts-ignore
|
||||||
|
weekNumber > currentWeek
|
||||||
|
? 'upcoming'
|
||||||
|
: 'completed'
|
||||||
|
return (
|
||||||
|
<WeekTab
|
||||||
|
key={week}
|
||||||
|
week={week}
|
||||||
|
matches={sortedMatches}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
'use client'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Eye, EyeOff } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export function Standings() {
|
||||||
|
const [showResults, setShowResults] = useState(false)
|
||||||
|
return (
|
||||||
|
<section id='results' className='container py-8 md:py-12'>
|
||||||
|
<div className='mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center'>
|
||||||
|
<h2 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
|
||||||
|
Tournament Standings
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-[85%] text-muted-foreground leading-normal sm:text-lg sm:leading-7'>
|
||||||
|
Current standings after Week 1
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowResults(!showResults)}
|
||||||
|
className='mt-4'
|
||||||
|
>
|
||||||
|
{showResults ? (
|
||||||
|
<>
|
||||||
|
<EyeOff className='mr-2 h-4 w-4' />
|
||||||
|
Hide Results
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Eye className='mr-2 h-4 w-4' />
|
||||||
|
Show Results
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { SiBluesky, SiYoutube } from '@icons-pack/react-simple-icons'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export function StayUpdated() {
|
||||||
|
return (
|
||||||
|
<section className='bg-muted py-12 md:py-16'>
|
||||||
|
<div className='container flex flex-col items-center justify-center gap-4 text-center'>
|
||||||
|
<h2 className='font-bold text-3xl tracking-tighter sm:text-4xl'>
|
||||||
|
Stay Updated
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-[600px] text-muted-foreground md:text-xl/relaxed'>
|
||||||
|
Follow the official channels for weekly recaps, schedules, and
|
||||||
|
standings.
|
||||||
|
</p>
|
||||||
|
<div className='mt-4 flex flex-col gap-4 sm:flex-row'>
|
||||||
|
<Link
|
||||||
|
href='https://youtube.com/@ZainoTVLive'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size='lg'
|
||||||
|
className='w-full bg-red-600 text-white hover:bg-red-700 sm:w-auto'
|
||||||
|
>
|
||||||
|
<SiYoutube className='ml-2 h-4 w-4' />
|
||||||
|
YouTube Channel
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href='https://bsky.app/profile/majorleaguebalatro.bsky.social'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<Button variant='outline' size='lg' className='w-full sm:w-auto'>
|
||||||
|
<SiBluesky className='ml-2 h-4 w-4' />
|
||||||
|
Bluesky Updates
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Trophy, Users } from 'lucide-react'
|
||||||
|
|
||||||
|
export function TournamentFormat() {
|
||||||
|
return (
|
||||||
|
<section id='format' className='container py-8 md:py-12'>
|
||||||
|
<div className='mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center'>
|
||||||
|
<h2 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
|
||||||
|
Tournament Format
|
||||||
|
</h2>
|
||||||
|
<p className='max-w-[85%] text-muted-foreground leading-normal sm:text-lg sm:leading-7'>
|
||||||
|
Major League Balatro features 12 creators competing in a structured
|
||||||
|
league format.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mx-auto mt-8 grid max-w-5xl gap-8 md:grid-cols-2'>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className='flex items-center gap-2'>
|
||||||
|
<Users className='h-5 w-5 text-red-500' />
|
||||||
|
League Structure
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='space-y-4'>
|
||||||
|
<ul className='space-y-2'>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• 12 creators split into 2 divisions (Blue & Red)
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• Each creator plays 5 matches (one against each division
|
||||||
|
member)
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• 1st place in each division advances to Final Four
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• 2nd and 3rd place enter a playoff round robin
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• Top 2 from playoff round robin advance to Final Four
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• Finals on Saturday, May 17th, 2025
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className='flex items-center gap-2'>
|
||||||
|
<Trophy className='h-5 w-5 text-red-500' />
|
||||||
|
Match Format
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='space-y-4'>
|
||||||
|
<ul className='space-y-2'>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• League matches: Best of 3 games
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• Playoff matches: Best of 5 games
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• 1 point per game win + 1 point for match win
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• Alternating deck and stake selection
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• Vanilla ruleset (no multiplayer jokers)
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-start gap-2'>
|
||||||
|
<span className='font-medium'>
|
||||||
|
• PvP battles after Ante-1 where lower score loses a life
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
456
src/app/(home)/major-league-balatro/_constants/matches.ts
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
import type { Match } from '../types'
|
||||||
|
|
||||||
|
export const matches: Match[] = [
|
||||||
|
// Week 1
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'roffle',
|
||||||
|
player2Id: 'haelian',
|
||||||
|
date: 'April 9, 2025',
|
||||||
|
time: '3:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-09T15:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2428370737?t=01h26m15s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2428334601?t=02h10m32s',
|
||||||
|
completed: true,
|
||||||
|
week: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'zainotv',
|
||||||
|
player2Id: 'bear',
|
||||||
|
date: 'April 7, 2025',
|
||||||
|
time: '8:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-07T20:00:00-04:00'),
|
||||||
|
vod1: 'https://youtu.be/NCsQ3PePAdc',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2426932234?t=00h20m47s',
|
||||||
|
completed: true,
|
||||||
|
week: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'gothic',
|
||||||
|
player2Id: 'neato',
|
||||||
|
date: 'April 9, 2025',
|
||||||
|
time: '1:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-09T13:00:00-04:00'),
|
||||||
|
vod1: 'https://youtu.be/BAcx4fih1V4',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2428289132?t=01h56m48s',
|
||||||
|
completed: true,
|
||||||
|
week: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'drspectred',
|
||||||
|
player2Id: 'malf',
|
||||||
|
date: 'April 7, 2025',
|
||||||
|
time: '11:00 AM EDT',
|
||||||
|
datetime: new Date('2025-04-07T11:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2426535670?t=01h08m29s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2426478651?t=02h52m39s',
|
||||||
|
completed: true,
|
||||||
|
week: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'skoottie',
|
||||||
|
player2Id: 'nandre',
|
||||||
|
date: 'April 7, 2025',
|
||||||
|
time: '9:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-07T21:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2426992443?t=00h15m19s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2426985590?t=00h22m44s',
|
||||||
|
completed: true,
|
||||||
|
week: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'edzy',
|
||||||
|
player2Id: 'seadubbs',
|
||||||
|
date: 'April 8, 2025',
|
||||||
|
time: '6:30 PM EDT',
|
||||||
|
datetime: new Date('2025-04-08T18:30:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2427881454?t=01h13m54s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2427647925?t=01h51m03s',
|
||||||
|
completed: true,
|
||||||
|
week: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Week 2
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'roffle',
|
||||||
|
player2Id: 'zainotv',
|
||||||
|
date: 'April 15, 2025',
|
||||||
|
time: '5:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-15T17:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2433708429?t=2h31m42s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2433805983?t=0h29m52s',
|
||||||
|
completed: true,
|
||||||
|
week: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'haelian',
|
||||||
|
player2Id: 'gothic',
|
||||||
|
date: 'April 16, 2025',
|
||||||
|
time: '3:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-16T15:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2434505224?t=2h4m35s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2434576019?t=0h38m11s',
|
||||||
|
completed: true,
|
||||||
|
week: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'bear',
|
||||||
|
player2Id: 'neato',
|
||||||
|
date: 'April 16, 2025',
|
||||||
|
time: '1:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-16T13:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2434505033?t=0h9m42s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2434499724?t=0h16m5s',
|
||||||
|
completed: true,
|
||||||
|
week: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'drspectred',
|
||||||
|
player2Id: 'skoottie',
|
||||||
|
date: 'April 14, 2025',
|
||||||
|
time: '1:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-14T13:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2432780721?t=0h6m52s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2432776421?t=0h12m29s',
|
||||||
|
completed: true,
|
||||||
|
week: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'malf',
|
||||||
|
player2Id: 'edzy',
|
||||||
|
date: 'April 17, 2025',
|
||||||
|
time: '2:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-17T14:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2435238972?t=4h54m48s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2435320190?t=2h37m18s',
|
||||||
|
completed: true,
|
||||||
|
week: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'nandre',
|
||||||
|
player2Id: 'seadubbs',
|
||||||
|
date: 'April 15, 2025',
|
||||||
|
time: '6:30 PM EDT',
|
||||||
|
datetime: new Date('2025-04-15T18:30:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2433882453?t=0h11m14s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2433878480?t=0h16m39s',
|
||||||
|
completed: true,
|
||||||
|
week: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Week 3
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'roffle',
|
||||||
|
player2Id: 'bear',
|
||||||
|
date: 'April 21, 2025',
|
||||||
|
time: '5:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-21T17:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2439002527?t=2h35m39s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2439115722?t=0h9m57s',
|
||||||
|
completed: true,
|
||||||
|
week: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'zainotv',
|
||||||
|
player2Id: 'gothic',
|
||||||
|
date: 'April 21, 2025',
|
||||||
|
time: '4:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-21T16:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2439061383?t=0h19m32s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2439069703?t=0h8m18s',
|
||||||
|
completed: true,
|
||||||
|
week: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'haelian',
|
||||||
|
player2Id: 'neato',
|
||||||
|
date: 'April 23, 2025',
|
||||||
|
time: '1:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-23T13:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2440603518?t=0h42m56s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2440622793?t=0h15m35s',
|
||||||
|
completed: true,
|
||||||
|
week: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 16,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'drspectred',
|
||||||
|
player2Id: 'nandre',
|
||||||
|
date: 'April 22, 2025',
|
||||||
|
time: '1:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-22T13:00:00-04:00'),
|
||||||
|
vod1: 'https://www.twitch.tv/videos/2439955669?t=0h26m44s',
|
||||||
|
vod2: 'https://www.twitch.tv/videos/2439965151?t=0h13m20s',
|
||||||
|
completed: true,
|
||||||
|
week: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'malf',
|
||||||
|
player2Id: 'seadubbs',
|
||||||
|
date: 'April 25, 2025',
|
||||||
|
time: '11:00 AM EDT',
|
||||||
|
datetime: new Date('2025-04-25T11:00:00-04:00'),
|
||||||
|
completed: false,
|
||||||
|
week: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 18,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'skoottie',
|
||||||
|
player2Id: 'edzy',
|
||||||
|
date: 'April 25, 2025',
|
||||||
|
time: '3:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-25T15:00:00-04:00'),
|
||||||
|
completed: false,
|
||||||
|
week: 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Week 4
|
||||||
|
{
|
||||||
|
id: 19,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'roffle',
|
||||||
|
player2Id: 'gothic',
|
||||||
|
date: 'April 29, 2025',
|
||||||
|
time: '5:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-29T17:00:00-04:00'),
|
||||||
|
completed: false,
|
||||||
|
week: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'haelian',
|
||||||
|
player2Id: 'bear',
|
||||||
|
date: 'April 28, 2025',
|
||||||
|
time: '5:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-28T17:00:00-04:00'),
|
||||||
|
completed: false,
|
||||||
|
week: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
division: 'Blue',
|
||||||
|
player1Id: 'zainotv',
|
||||||
|
player2Id: 'neato',
|
||||||
|
date: 'May 1, 2025',
|
||||||
|
time: '5:00 PM EDT',
|
||||||
|
datetime: new Date('2025-05-01T17:00:00-04:00'),
|
||||||
|
completed: false,
|
||||||
|
week: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 22,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'drspectred',
|
||||||
|
player2Id: 'edzy',
|
||||||
|
date: 'April 30, 2025',
|
||||||
|
time: '1:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-30T13:00:00-04:00'),
|
||||||
|
completed: false,
|
||||||
|
week: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 23,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'malf',
|
||||||
|
player2Id: 'nandre',
|
||||||
|
date: 'April 30, 2025',
|
||||||
|
time: '2:00 PM EDT',
|
||||||
|
datetime: new Date('2025-04-30T14:00:00-04:00'),
|
||||||
|
completed: false,
|
||||||
|
week: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 24,
|
||||||
|
division: 'Red',
|
||||||
|
player1Id: 'skoottie',
|
||||||
|
player2Id: 'seadubbs',
|
||||||
|
date: 'May 2, 2025',
|
||||||
|
time: '3:00 PM EDT',
|
||||||
|
datetime: new Date('2025-05-02T15:00:00-04:00'),
|
||||||
|
completed: false,
|
||||||
|
week: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
// // Week 5
|
||||||
|
// {
|
||||||
|
// id: 25,
|
||||||
|
// division: 'Blue',
|
||||||
|
// player1Id: 'roffle',
|
||||||
|
// player2Id: 'neato',
|
||||||
|
// date: 'May 5, 2025',
|
||||||
|
// time: '5:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-05T17:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 5,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 26,
|
||||||
|
// division: 'Blue',
|
||||||
|
// player1Id: 'haelian',
|
||||||
|
// player2Id: 'zainotv',
|
||||||
|
// date: 'May 7, 2025',
|
||||||
|
// time: '3:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-07T15:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 5,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 27,
|
||||||
|
// division: 'Blue',
|
||||||
|
// player1Id: 'bear',
|
||||||
|
// player2Id: 'gothic',
|
||||||
|
// date: 'May 8, 2025',
|
||||||
|
// time: '1:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-08T13:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 5,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 28,
|
||||||
|
// division: 'Red',
|
||||||
|
// player1Id: 'drspectred',
|
||||||
|
// player2Id: 'seadubbs',
|
||||||
|
// date: 'May 5, 2025',
|
||||||
|
// time: '1:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-05T13:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 5,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 29,
|
||||||
|
// division: 'Red',
|
||||||
|
// player1Id: 'malf',
|
||||||
|
// player2Id: 'skoottie',
|
||||||
|
// date: 'May 6, 2025',
|
||||||
|
// time: '2:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-06T14:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 5,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 30,
|
||||||
|
// division: 'Red',
|
||||||
|
// player1Id: 'nandre',
|
||||||
|
// player2Id: 'edzy',
|
||||||
|
// date: 'May 9, 2025',
|
||||||
|
// time: '6:30 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-09T18:30:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 5,
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// // Play-in Week
|
||||||
|
// {
|
||||||
|
// id: 31,
|
||||||
|
// division: 'Playoff',
|
||||||
|
// player1Id: 'tbd',
|
||||||
|
// player2Id: 'tbd',
|
||||||
|
// date: 'May 12, 2025',
|
||||||
|
// time: '5:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-12T17:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 'Play-in',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 32,
|
||||||
|
// division: 'Playoff',
|
||||||
|
// player1Id: 'tbd',
|
||||||
|
// player2Id: 'tbd',
|
||||||
|
// date: 'May 13, 2025',
|
||||||
|
// time: '5:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-13T17:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 'Play-in',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 33,
|
||||||
|
// division: 'Playoff',
|
||||||
|
// player1Id: 'tbd',
|
||||||
|
// player2Id: 'tbd',
|
||||||
|
// date: 'May 14, 2025',
|
||||||
|
// time: '5:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-14T17:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 'Play-in',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 34,
|
||||||
|
// division: 'Playoff',
|
||||||
|
// player1Id: 'tbd',
|
||||||
|
// player2Id: 'tbd',
|
||||||
|
// date: 'May 15, 2025',
|
||||||
|
// time: '5:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-15T17:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 'Play-in',
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// // Finals
|
||||||
|
// {
|
||||||
|
// id: 35,
|
||||||
|
// division: 'Finals',
|
||||||
|
// player1Id: 'tbd',
|
||||||
|
// player2Id: 'tbd',
|
||||||
|
// date: 'May 17, 2025',
|
||||||
|
// time: '11:00 AM EDT',
|
||||||
|
// datetime: new Date('2025-05-17T11:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 'Finals',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 36,
|
||||||
|
// division: 'Finals',
|
||||||
|
// player1Id: 'tbd',
|
||||||
|
// player2Id: 'tbd',
|
||||||
|
// date: 'May 17, 2025',
|
||||||
|
// time: '1:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-17T13:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 'Finals',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 37,
|
||||||
|
// division: 'Finals',
|
||||||
|
// player1Id: 'tbd',
|
||||||
|
// player2Id: 'tbd',
|
||||||
|
// date: 'May 17, 2025',
|
||||||
|
// time: '3:00 PM EDT',
|
||||||
|
// datetime: new Date('2025-05-17T15:00:00-04:00'),
|
||||||
|
// completed: false,
|
||||||
|
// week: 'Finals',
|
||||||
|
// },
|
||||||
|
]
|
||||||
123
src/app/(home)/major-league-balatro/_constants/players.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { Player } from '../types'
|
||||||
|
|
||||||
|
export const players: Record<string, Player> = {
|
||||||
|
roffle: {
|
||||||
|
id: 'roffle',
|
||||||
|
name: 'Roffle',
|
||||||
|
division: 'Blue',
|
||||||
|
picture: '/images/players/roffle.png',
|
||||||
|
socials: {
|
||||||
|
twitch: 'roffle',
|
||||||
|
youtube: 'RoffleLite',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
haelian: {
|
||||||
|
id: 'haelian',
|
||||||
|
name: 'Haelian',
|
||||||
|
division: 'Blue',
|
||||||
|
picture: '/images/players/haelian.png',
|
||||||
|
socials: {
|
||||||
|
twitch: 'haelian',
|
||||||
|
youtube: 'Haelian',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
zainotv: {
|
||||||
|
id: 'zainotv',
|
||||||
|
name: 'ZainoTV',
|
||||||
|
division: 'Blue',
|
||||||
|
picture: '/images/players/zaino.png',
|
||||||
|
socials: {
|
||||||
|
twitch: 'zainotv',
|
||||||
|
youtube: 'ZainoTVLive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bear: {
|
||||||
|
id: 'bear',
|
||||||
|
name: 'Bear',
|
||||||
|
division: 'Blue',
|
||||||
|
picture: '/images/players/bear.png',
|
||||||
|
socials: {
|
||||||
|
twitch: 'belenosbear',
|
||||||
|
youtube: 'BelenosBear',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gothic: {
|
||||||
|
id: 'gothic',
|
||||||
|
name: 'Gothic',
|
||||||
|
division: 'Blue',
|
||||||
|
picture: '/images/players/gothic.png',
|
||||||
|
socials: {
|
||||||
|
twitch: 'gothiclorduk',
|
||||||
|
youtube: 'GothicLordUK',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
neato: {
|
||||||
|
id: 'neato',
|
||||||
|
name: 'NEATO',
|
||||||
|
division: 'Blue',
|
||||||
|
picture: '/images/players/neato.jpg',
|
||||||
|
socials: {
|
||||||
|
twitch: 'neato',
|
||||||
|
youtube: 'neatoqueen',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
drspectred: {
|
||||||
|
id: 'drspectred',
|
||||||
|
name: 'DrSpectred',
|
||||||
|
division: 'Red',
|
||||||
|
picture: '/images/players/doc.png',
|
||||||
|
socials: {
|
||||||
|
twitch: 'drspectred',
|
||||||
|
youtube: 'drspectred',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
malf: {
|
||||||
|
id: 'malf',
|
||||||
|
name: 'MALF',
|
||||||
|
division: 'Red',
|
||||||
|
picture: '/images/players/malf.png',
|
||||||
|
socials: {
|
||||||
|
twitch: 'michaelalfox',
|
||||||
|
youtube: 'michaelalfox',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
skoottie: {
|
||||||
|
id: 'skoottie',
|
||||||
|
name: 'Skoottie',
|
||||||
|
division: 'Red',
|
||||||
|
picture: '/images/players/skoottie.jpg',
|
||||||
|
socials: {
|
||||||
|
twitch: 'skoottie',
|
||||||
|
youtube: 'Skoottie',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nandre: {
|
||||||
|
id: 'nandre',
|
||||||
|
name: 'Nandre',
|
||||||
|
division: 'Red',
|
||||||
|
picture: '/images/players/nandre.jpg',
|
||||||
|
socials: {
|
||||||
|
twitch: 'nandre',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edzy: {
|
||||||
|
id: 'edzy',
|
||||||
|
name: 'Edzy',
|
||||||
|
division: 'Red',
|
||||||
|
picture: '/images/players/edzy.png',
|
||||||
|
socials: {
|
||||||
|
twitch: 'edzyttv',
|
||||||
|
youtube: 'EdzyTTV',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seadubbs: {
|
||||||
|
id: 'seadubbs',
|
||||||
|
name: 'Seadubbs',
|
||||||
|
division: 'Red',
|
||||||
|
picture: '/images/players/seadubbs.png',
|
||||||
|
socials: {
|
||||||
|
twitch: 'seadubbs11',
|
||||||
|
youtube: 'seadubbs11',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
76
src/app/(home)/major-league-balatro/page.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { Standings } from '@/app/(home)/major-league-balatro/_components/standings'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Competitors } from './_components/competitors'
|
||||||
|
import { HeroSection } from './_components/hero'
|
||||||
|
import { NextMatchInfo } from './_components/next-match-info'
|
||||||
|
import { Organizer } from './_components/organizer'
|
||||||
|
import { PrizePool } from './_components/prize-pool'
|
||||||
|
import { MlbSchedule } from './_components/schedule'
|
||||||
|
import { StayUpdated } from './_components/stay-updated'
|
||||||
|
import { TournamentFormat } from './_components/tournament-format'
|
||||||
|
import { matches } from './_constants/matches'
|
||||||
|
|
||||||
|
export default function MLBPage() {
|
||||||
|
const currentDate = new Date()
|
||||||
|
|
||||||
|
// Update the next match calculation to filter out completed matches
|
||||||
|
const nextMatch = matches
|
||||||
|
.filter((match) => !match.completed && match.datetime > currentDate)
|
||||||
|
.sort((a, b) => a.datetime.getTime() - b.datetime.getTime())[0]
|
||||||
|
if (!nextMatch) {
|
||||||
|
return 'All matches have been played'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex min-h-screen flex-col'>
|
||||||
|
<main className='flex-1'>
|
||||||
|
<HeroSection />
|
||||||
|
|
||||||
|
<NextMatchInfo nextMatch={nextMatch} />
|
||||||
|
|
||||||
|
{/*<Standings />*/}
|
||||||
|
|
||||||
|
<TournamentFormat />
|
||||||
|
|
||||||
|
<Competitors />
|
||||||
|
|
||||||
|
<MlbSchedule matches={matches} />
|
||||||
|
|
||||||
|
<PrizePool />
|
||||||
|
|
||||||
|
<Organizer />
|
||||||
|
|
||||||
|
<StayUpdated />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className='border-t py-6 md:py-0'>
|
||||||
|
<div className='container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row'>
|
||||||
|
<p className='text-center text-muted-foreground text-sm leading-loose md:text-left'>
|
||||||
|
© {new Date().getFullYear()} Major League Balatro. All rights
|
||||||
|
reserved.
|
||||||
|
</p>
|
||||||
|
<div className='flex gap-4'>
|
||||||
|
<Link
|
||||||
|
href='/'
|
||||||
|
className='text-muted-foreground text-sm underline-offset-4 hover:underline'
|
||||||
|
>
|
||||||
|
Balatro Multiplayer
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href='/docs'
|
||||||
|
className='text-muted-foreground text-sm underline-offset-4 hover:underline'
|
||||||
|
>
|
||||||
|
Documentation
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href='/about'
|
||||||
|
className='text-muted-foreground text-sm underline-offset-4 hover:underline'
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
src/app/(home)/major-league-balatro/types.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { players } from './_constants/players'
|
||||||
|
|
||||||
|
export type Player = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
division: 'Blue' | 'Red'
|
||||||
|
picture: string
|
||||||
|
socials: {
|
||||||
|
twitch?: string
|
||||||
|
youtube?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Match = {
|
||||||
|
id: number
|
||||||
|
division: 'Blue' | 'Red' | 'Playoff' | 'Finals'
|
||||||
|
player1Id: keyof typeof players
|
||||||
|
player2Id: keyof typeof players
|
||||||
|
date: string
|
||||||
|
time: string
|
||||||
|
datetime: Date
|
||||||
|
vod1?: string
|
||||||
|
vod2?: string
|
||||||
|
completed: boolean
|
||||||
|
week: number | 'Play-in' | 'Finals'
|
||||||
|
}
|
||||||
|
export type WeekStatus =
|
||||||
|
| 'completed'
|
||||||
|
| 'current'
|
||||||
|
| 'upcoming'
|
||||||
|
| 'playoff'
|
||||||
|
| 'finals'
|
||||||
|
|
||||||
|
export type BadgeProps = {
|
||||||
|
variant: 'outline'
|
||||||
|
className: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WeekConfig = {
|
||||||
|
label: string
|
||||||
|
badgeProps?: BadgeProps
|
||||||
|
}
|
||||||
BIN
src/app/_assets/fonts/m6x11.ttf
Normal file
@@ -18,6 +18,16 @@ const links = [
|
|||||||
url: '/about',
|
url: '/about',
|
||||||
icon: <Info />,
|
icon: <Info />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'menu' as const,
|
||||||
|
text: 'Major League Balatro',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: 'About',
|
||||||
|
url: '/major-league-balatro',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Support Mod Development',
|
text: 'Support Mod Development',
|
||||||
url: 'https://ko-fi.com/virtualized/shop',
|
url: 'https://ko-fi.com/virtualized/shop',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { NextIntlClientProvider } from 'next-intl'
|
|||||||
import { getLocale } from 'next-intl/server'
|
import { getLocale } from 'next-intl/server'
|
||||||
import PlausibleProvider from 'next-plausible'
|
import PlausibleProvider from 'next-plausible'
|
||||||
import { Geist } from 'next/font/google'
|
import { Geist } from 'next/font/google'
|
||||||
|
import localFont from 'next/font/local'
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
template: '%s | Balatro Multiplayer',
|
template: '%s | Balatro Multiplayer',
|
||||||
@@ -24,6 +24,12 @@ const geist = Geist({
|
|||||||
variable: '--font-geist-sans',
|
variable: '--font-geist-sans',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const m6x11 = localFont({
|
||||||
|
src: './_assets/fonts/m6x11.ttf',
|
||||||
|
display: 'swap',
|
||||||
|
variable: '--font-m6x11',
|
||||||
|
})
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
@@ -31,7 +37,7 @@ export default async function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang={locale}
|
lang={locale}
|
||||||
className={`${geist.variable}`}
|
className={`${geist.variable} ${m6x11.variable}`}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ function getPlayerData(
|
|||||||
meaningful_games > 0 ? Math.floor((losses / meaningful_games) * 100) : 0,
|
meaningful_games > 0 ? Math.floor((losses / meaningful_games) * 100) : 0,
|
||||||
rank: playerLeaderboardEntry.rank,
|
rank: playerLeaderboardEntry.rank,
|
||||||
mmr: Math.round(playerLeaderboardEntry.mmr),
|
mmr: Math.round(playerLeaderboardEntry.mmr),
|
||||||
|
mmrChangeRaw: lastGame?.mmrChange,
|
||||||
mmrChange: `${(lastGame?.mmrChange ?? 0) >= 0 ? '+' : ''}${Math.round(
|
mmrChange: `${(lastGame?.mmrChange ?? 0) >= 0 ? '+' : ''}${Math.round(
|
||||||
lastGame?.mmrChange ?? 0
|
lastGame?.mmrChange ?? 0
|
||||||
)}`,
|
)}`,
|
||||||
@@ -93,8 +94,11 @@ export function StreamCardClient() {
|
|||||||
const isQueuing = playerState?.status === 'queuing'
|
const isQueuing = playerState?.status === 'queuing'
|
||||||
const opponentId = playerState?.currentMatch?.opponentId
|
const opponentId = playerState?.currentMatch?.opponentId
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center gap-2'} style={{ zoom: '200%' }}>
|
<div
|
||||||
<PlayerInfo playerData={playerData}>
|
className={'flex items-center justify-between gap-2 font-m6x11'}
|
||||||
|
style={{ zoom: '200%' }}
|
||||||
|
>
|
||||||
|
<PlayerInfo playerData={playerData} isInBattle={!!opponentId}>
|
||||||
{isQueuing && playerState.queueStartTime && (
|
{isQueuing && playerState.queueStartTime && (
|
||||||
<QueueTimer startTime={playerState.queueStartTime} />
|
<QueueTimer startTime={playerState.queueStartTime} />
|
||||||
)}
|
)}
|
||||||
@@ -160,7 +164,7 @@ function Opponent({ id }: { id: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const playerData = getPlayerData(rankedUserRank, games)
|
const playerData = getPlayerData(rankedUserRank, games)
|
||||||
return <PlayerInfo playerData={playerData} isReverse />
|
return <PlayerInfo playerData={playerData} isReverse isInBattle />
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlayerInfo({
|
function PlayerInfo({
|
||||||
@@ -168,10 +172,12 @@ function PlayerInfo({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
isReverse = false,
|
isReverse = false,
|
||||||
|
isInBattle = false,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
playerData: ReturnType<typeof getPlayerData>
|
playerData: ReturnType<typeof getPlayerData>
|
||||||
isReverse?: boolean
|
isReverse?: boolean
|
||||||
|
isInBattle?: boolean
|
||||||
} & ComponentPropsWithoutRef<'div'>) {
|
} & ComponentPropsWithoutRef<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -200,16 +206,27 @@ function PlayerInfo({
|
|||||||
<div className='flex items-center gap-1.5 border-slate-700 border-r px-2'>
|
<div className='flex items-center gap-1.5 border-slate-700 border-r px-2'>
|
||||||
<div>MMR:</div>
|
<div>MMR:</div>
|
||||||
<div className='font-bold'>{playerData.mmr}</div>
|
<div className='font-bold'>{playerData.mmr}</div>
|
||||||
<div className='text-emerald-400 text-sm'>{playerData.mmrChange}</div>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'!text-emerald-400 text-sm',
|
||||||
|
playerData.mmrChangeRaw &&
|
||||||
|
playerData.mmrChangeRaw < 0 &&
|
||||||
|
'!text-rose-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{playerData.mmrChange}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Win Rate */}
|
{/* Win Rate */}
|
||||||
<div className='flex items-center gap-1.5 text-nowrap border-slate-700 border-r px-2'>
|
{!isInBattle && (
|
||||||
<div className='text-nowrap'>Win Rate:</div>
|
<div className='flex items-center gap-1.5 text-nowrap border-slate-700 border-r px-2'>
|
||||||
<div className='text font-bold text-emerald-400'>
|
<div className='text-nowrap'>WR:</div>
|
||||||
{playerData.winRate}%
|
<div className='text font-bold text-emerald-400'>
|
||||||
|
{playerData.winRate}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Win/Loss */}
|
{/* Win/Loss */}
|
||||||
<div className='flex items-center gap-0.5 border-slate-700 border-r px-2'>
|
<div className='flex items-center gap-0.5 border-slate-700 border-r px-2'>
|
||||||
@@ -227,15 +244,17 @@ function PlayerInfo({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Streak */}
|
{/* Streak */}
|
||||||
<div
|
{!isInBattle && (
|
||||||
className={cn(
|
<div
|
||||||
'flex items-center gap-1.5 border-slate-700 px-2',
|
className={cn(
|
||||||
(children || isReverse) && 'border-r'
|
'flex items-center gap-1.5 border-slate-700 px-2',
|
||||||
)}
|
(children || isReverse) && 'border-r'
|
||||||
>
|
)}
|
||||||
<div>Streak:</div>
|
>
|
||||||
<div className='font-bold text-emerald-400'>{playerData.streak}</div>
|
<div>Streak:</div>
|
||||||
</div>
|
<div className='font-bold text-emerald-400'>{playerData.streak}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
69
src/components/countdown-timer.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
'use client'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export function CountdownTimer({ nextMatch }: { nextMatch: any }) {
|
||||||
|
const [timeLeft, setTimeLeft] = useState({
|
||||||
|
days: 0,
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const calculateTimeLeft = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const matchTime = new Date(nextMatch.datetime)
|
||||||
|
const difference = matchTime.getTime() - now.getTime()
|
||||||
|
|
||||||
|
if (difference <= 0) {
|
||||||
|
return {
|
||||||
|
days: 0,
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
|
||||||
|
hours: Math.floor(
|
||||||
|
(difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
|
||||||
|
),
|
||||||
|
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
|
||||||
|
seconds: Math.floor((difference % (1000 * 60)) / 1000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeLeft(calculateTimeLeft())
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTimeLeft(calculateTimeLeft())
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [nextMatch])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<div className='grid w-full max-w-xl grid-cols-4 gap-2 md:gap-4'>
|
||||||
|
{[
|
||||||
|
{ value: timeLeft.days, label: 'Days' },
|
||||||
|
{ value: timeLeft.hours, label: 'Hours' },
|
||||||
|
{ value: timeLeft.minutes, label: 'Minutes' },
|
||||||
|
{ value: timeLeft.seconds, label: 'Seconds' },
|
||||||
|
].map((item, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<div className='relative flex flex-col items-center justify-center rounded-lg border bg-card p-3 shadow-sm md:p-4'>
|
||||||
|
<span className='font-bold text-2xl tabular-nums md:text-4xl lg:text-5xl'>
|
||||||
|
{item.value.toString().padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<span className='mt-1 text-muted-foreground text-xs md:text-sm'>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client"
|
'use client'
|
||||||
|
|
||||||
import * as React from "react"
|
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
import type * as React from 'react'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
function Avatar({
|
function Avatar({
|
||||||
className,
|
className,
|
||||||
@@ -11,9 +11,9 @@ function Avatar({
|
|||||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
data-slot="avatar"
|
data-slot='avatar'
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -27,8 +27,8 @@ function AvatarImage({
|
|||||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Image
|
<AvatarPrimitive.Image
|
||||||
data-slot="avatar-image"
|
data-slot='avatar-image'
|
||||||
className={cn("aspect-square size-full", className)}
|
className={cn('aspect-square size-full object-cover!', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -40,9 +40,9 @@ function AvatarFallback({
|
|||||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
data-slot="avatar-fallback"
|
data-slot='avatar-fallback'
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
'flex size-full items-center justify-center rounded-full bg-muted',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
|
--font-m6x11: var(--font-m6x11);
|
||||||
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
||||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
}
|
}
|
||||||
|
|||||||