diff --git a/.gitignore b/.gitignore index fc6eb82..a0ddddd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - +.thing # dependencies /node_modules /.pnp diff --git a/content/docs/getting-started/multiplayer-balance-changes.mdx b/content/docs/getting-started/multiplayer-balance-changes.mdx deleted file mode 100644 index ce49e70..0000000 --- a/content/docs/getting-started/multiplayer-balance-changes.mdx +++ /dev/null @@ -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. - - - These jokers can be removed by unchecking the **"Enable Multiplayer Cards"** option in the lobby options. - - -### 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. - - - These planet cards can be removed by unchecking the **"Enable Multiplayer Cards"** option in the lobby options. - - -## 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) diff --git a/content/docs/meta.json b/content/docs/meta.json index c3588d8..149a674 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -2,6 +2,7 @@ "pages": [ "index", "getting-started", + "multiplayer-content", "rulesets", "ranked-matchmaking", "advanced" diff --git a/content/docs/multiplayer-content/jokers.mdx b/content/docs/multiplayer-content/jokers.mdx new file mode 100644 index 0000000..e63aa76 --- /dev/null +++ b/content/docs/multiplayer-content/jokers.mdx @@ -0,0 +1,5 @@ +--- +title: Jokers +description: Multiplayer-specific Jokers +--- + diff --git a/content/docs/multiplayer-content/meta.json b/content/docs/multiplayer-content/meta.json new file mode 100644 index 0000000..de2ea3f --- /dev/null +++ b/content/docs/multiplayer-content/meta.json @@ -0,0 +1,4 @@ +{ + "pages": ["jokers"], + "defaultOpen": true +} diff --git a/content/docs/ranked-matchmaking/faq.mdx b/content/docs/ranked-matchmaking/faq.mdx new file mode 100644 index 0000000..b70da5f --- /dev/null +++ b/content/docs/ranked-matchmaking/faq.mdx @@ -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? + diff --git a/content/docs/ranked-matchmaking/introduction.mdx b/content/docs/ranked-matchmaking/introduction.mdx new file mode 100644 index 0000000..252e141 --- /dev/null +++ b/content/docs/ranked-matchmaking/introduction.mdx @@ -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. + diff --git a/content/docs/ranked-matchmaking/meta.json b/content/docs/ranked-matchmaking/meta.json new file mode 100644 index 0000000..2bc3fa1 --- /dev/null +++ b/content/docs/ranked-matchmaking/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Ranked Matchmaking", + "pages": ["introduction", "multiplayer-balance-changes", "faq"], + "defaultOpen": true +} diff --git a/content/docs/ranked-matchmaking/multiplayer-balance-changes.mdx b/content/docs/ranked-matchmaking/multiplayer-balance-changes.mdx new file mode 100644 index 0000000..f2bd925 --- /dev/null +++ b/content/docs/ranked-matchmaking/multiplayer-balance-changes.mdx @@ -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. + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
JokerDescriptionNotes
+ #### Defensive Joker [toc] + + + +125 Chips for every life less than your Nemesis. + + There is a bug where this joker might appear in single player mode. To fix this, disable the multiplayer + mod when playing single player. +
+ #### Skip-Off [toc] + + + +1 Hands and +1 Discards per additional blind skipped compared to your + Nemesis. +
+ #### Let's Go Gambling [toc] + + +
+ 1 in 4 chance for X4 Mult and $10 + 1 in 8 chance to give your Nemesis $5 +
+
+ The effect applies on each hand played. +
+ #### Speedrun [toc] + + + If you reach a PvP Blind before your Nemesis, create a + random Spectral card. Must have room. + + There is a bug where the spectral card might not be created, but is is very rare. We're working on a + fix. +
+ #### Conjoined Joker [toc] + + + While in a PvP Blind, gain + X1 Mult for every Hand + your Nemesis has left + (Max X5 Mult) +
+ #### Penny Pincher [toc] + + + At start of shop, gain + $1 for every $3 your Nemesis spent last shop +
+ #### Taxes [toc] + + + This Joker gains +5 Mult each time your Nemesis sells a card +
+ #### Magnet [toc] + + + After 2 rounds, sell this card to Copy your Nemesis' highest sell cost Joker. Does not + copy Joker state. + + If your Nemesis has multiple Jokers that have the highest sell cost, a random one will be copied. +
+ #### Pizza [toc] + + +
+ +6 Discards for all players + -1 Discard when any player selects a blind + Eaten when your Nemesis skips +
+
+ #### Pacifist [toc] + + +
+ X10 Mult while not in a PvP Blind +
+
+
+ + + These jokers can be removed by unchecking the **"Enable Multiplayer Cards"** option in the lobby options. + + +### Disabled Jokers +The following jokers are **unavailable** in multiplayer due to their interaction with boss blinds: + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
JokerDescription
+ #### Chicot [toc] + + + Disables effect of every Boss Blind +
+ #### Matador [toc] + + + Earn $8 if played hand triggers the Boss Blind ability +
+ #### Mr. Bones [toc] + + + Prevents Death if chips scored are at least 25% of required chips. + Self destructs +
+ #### Luchador [toc] + + + Sell this card to disable the current Boss Blind +
+
+ + +### Modified Jokers + +These jokers are modified versions of the original jokers. + +
+ + + + + + + + + + + + + +
JokerDescription
+ #### Hanging Chad [toc] + + + Retrigger first and second played cards used in + scoring 1 additional time +
+
+ +## Planet cards + +
+ + + + + + + + + + + + + +
JokerDescription
+ ### Asteroid [toc] + + + Removes one level from your Nemesis' highest level poker hand. +
+
+ + + + + These planet cards can be removed by unchecking the **"Enable Multiplayer Cards"** option in the lobby options. + + +## 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) diff --git a/public/images/players/bear.png b/public/images/players/bear.png new file mode 100644 index 0000000..5940ea7 Binary files /dev/null and b/public/images/players/bear.png differ diff --git a/public/images/players/doc.png b/public/images/players/doc.png new file mode 100644 index 0000000..a21bb08 Binary files /dev/null and b/public/images/players/doc.png differ diff --git a/public/images/players/edzy.png b/public/images/players/edzy.png new file mode 100644 index 0000000..387c451 Binary files /dev/null and b/public/images/players/edzy.png differ diff --git a/public/images/players/gothic.png b/public/images/players/gothic.png new file mode 100644 index 0000000..b17c8c5 Binary files /dev/null and b/public/images/players/gothic.png differ diff --git a/public/images/players/haelian.png b/public/images/players/haelian.png new file mode 100644 index 0000000..7ea5b1c Binary files /dev/null and b/public/images/players/haelian.png differ diff --git a/public/images/players/malf.png b/public/images/players/malf.png new file mode 100644 index 0000000..9318068 Binary files /dev/null and b/public/images/players/malf.png differ diff --git a/public/images/players/nandre.jpg b/public/images/players/nandre.jpg new file mode 100644 index 0000000..f35803e Binary files /dev/null and b/public/images/players/nandre.jpg differ diff --git a/public/images/players/neato.jpg b/public/images/players/neato.jpg new file mode 100644 index 0000000..5d94617 Binary files /dev/null and b/public/images/players/neato.jpg differ diff --git a/public/images/players/roffle.png b/public/images/players/roffle.png new file mode 100644 index 0000000..276e1cf Binary files /dev/null and b/public/images/players/roffle.png differ diff --git a/public/images/players/seadubbs.png b/public/images/players/seadubbs.png new file mode 100644 index 0000000..3f60a2d Binary files /dev/null and b/public/images/players/seadubbs.png differ diff --git a/public/images/players/skoottie.jpg b/public/images/players/skoottie.jpg new file mode 100644 index 0000000..759aa6e Binary files /dev/null and b/public/images/players/skoottie.jpg differ diff --git a/public/images/players/zaino.png b/public/images/players/zaino.png new file mode 100644 index 0000000..40caa64 Binary files /dev/null and b/public/images/players/zaino.png differ diff --git a/src/app/(home)/major-league-balatro/_components/competitors.tsx b/src/app/(home)/major-league-balatro/_components/competitors.tsx new file mode 100644 index 0000000..6c28b00 --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/competitors.tsx @@ -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 ( +
+
+

+ The Competitors +

+

+ 12 talented creators split into Blue and Red divisions. +

+
+ + + + Blue Division + Red Division + + +
+ {Object.values(players) + .filter((player) => player.division === 'Blue') + .map((creator) => ( + + ))} +
+
+ +
+ {Object.values(players) + .filter( + (player) => player.division === 'Red' && player.id !== 'tbd' + ) + .map((creator) => ( + + ))} +
+
+
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/hero.tsx b/src/app/(home)/major-league-balatro/_components/hero.tsx new file mode 100644 index 0000000..3908ea7 --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/hero.tsx @@ -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 ( +
+
+ + Season 1 + +

+ Major League Balatro +

+

+ 12 creators. 2 divisions. 1 champion. The ultimate Balatro multiplayer + tournament. +

+ +
+
+ + 12 + + Creators +
+
+ + 5 + + Weeks +
+
+ + $500+ + + Prize Pool +
+
+ + May 17 + + Finals +
+
+ +
+ + + + + + +
+
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/next-match-info-card.tsx b/src/app/(home)/major-league-balatro/_components/next-match-info-card.tsx new file mode 100644 index 0000000..c9e483f --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/next-match-info-card.tsx @@ -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 ( +
+
+
+
+ {typeof week === 'string' ? week : `Week ${week}`} +
+
+ Best of {bestOf} +
+
+ +
+

+
+ + {player1.name} +
+ vs +
+ {player2.name} + +
+

+
+ + + + + + + + {player1.name} + + + + + {player2.name} + + + + +
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/next-match-info.tsx b/src/app/(home)/major-league-balatro/_components/next-match-info.tsx new file mode 100644 index 0000000..52d1d74 --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/next-match-info.tsx @@ -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 ( + + + + + Tournament Schedule + + + +
+ +
+

+ No upcoming matches scheduled at this time. +

+

+ Check back later for updates or view past matches below. +

+
+
+
+ ) + } + + 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 ( + +
+
+
+

+ Next Match Countdown +

+
+ +

+ {nextMatch.date} - {nextMatch.time} +

+
+
+ + + + +
+
+
+ ) +} + +function SectionContainer({ children }: PropsWithChildren) { + return ( +
+
{children}
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/organizer.tsx b/src/app/(home)/major-league-balatro/_components/organizer.tsx new file mode 100644 index 0000000..e58872f --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/organizer.tsx @@ -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 ( +
+
+

+ About the Organizer +

+
+ +
+
+
+ +
+
+
+

ZainoTV

+

+ 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. +

+
+ + + + + + +
+
+
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/player-avatar.tsx b/src/app/(home)/major-league-balatro/_components/player-avatar.tsx new file mode 100644 index 0000000..ecbe107 --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/player-avatar.tsx @@ -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 ( + + + {initials} + + ) +} + +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() +} diff --git a/src/app/(home)/major-league-balatro/_components/player-card.tsx b/src/app/(home)/major-league-balatro/_components/player-card.tsx new file mode 100644 index 0000000..b45dbdf --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/player-card.tsx @@ -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 ( + +
+
+ +
+
+ +

{player.name}

+
+
@{player.socials.twitch || player.socials.youtube || ''}
+
+ {player.socials.twitch && ( + + + + )} + {player.socials.youtube && ( + + + + )} +
+
+
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/prize-pool.tsx b/src/app/(home)/major-league-balatro/_components/prize-pool.tsx new file mode 100644 index 0000000..7bba33d --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/prize-pool.tsx @@ -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 ( +
+
+

+ Prize Pool +

+

+ Playing for charity and glory. +

+
+ +
+ + + + + Charity Prize Pool + + + +
+ $500+ +
+

+ 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. +

+ +
+ 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. +
+
+
+
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/schedule.tsx b/src/app/(home)/major-league-balatro/_components/schedule.tsx new file mode 100644 index 0000000..a13826e --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/schedule.tsx @@ -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 = { + 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 ( + + {text} + + ) +} + +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 ( +
+ + {division} Division + +
+ ) +} + +type MatchDateTimeProps = { + date: string + time: string +} + +const MatchDateTime = ({ date, time }: MatchDateTimeProps) => ( +
+ + {date} + + {time} +
+) + +type VodButtonProps = { + url: string + player: string +} + +const VodButton = ({ url, player }: VodButtonProps) => ( + + + +) + +type LiveButtonProps = { + username: string +} + +const LiveButton = ({ username }: LiveButtonProps) => ( + + + +) + +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 ( + +
+
+
+ {date} • {time} +
+
+
+
+
+
+ + +
+
+

+ {player1.name} vs {player2.name} +

+
+
+
+ {completed ? ( + <> + + + + + + {vod1 && ( + + + + {player1.name} + + + )} + {vod2 && ( + + + + {player2.name} + + + )} + + + + ) : ( + + + + + + + + {player1.name} + + + + + {player2.name} + + + + + )} +
+
+
+
+
+ ) +} + +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 ( + +
+

{weekConfig.label}

+ +
+ + {filteredMatches.map((match, index) => ( + + ))} +
+ ) +} +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 ( +
+
+

+ Tournament Schedule +

+

+ All matches from April to May 2025 +

+
+ +
+ + + {Object.keys(WEEK_CONFIG).map((week) => ( + + Week {week} + + ))} + + + {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 ( + + ) + })} + +
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/standings.tsx b/src/app/(home)/major-league-balatro/_components/standings.tsx new file mode 100644 index 0000000..11b5267 --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/standings.tsx @@ -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 ( +
+
+

+ Tournament Standings +

+

+ Current standings after Week 1 +

+ + +
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/stay-updated.tsx b/src/app/(home)/major-league-balatro/_components/stay-updated.tsx new file mode 100644 index 0000000..fb741a6 --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/stay-updated.tsx @@ -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 ( +
+
+

+ Stay Updated +

+

+ Follow the official channels for weekly recaps, schedules, and + standings. +

+
+ + + + + + +
+
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_components/tournament-format.tsx b/src/app/(home)/major-league-balatro/_components/tournament-format.tsx new file mode 100644 index 0000000..20a8f9f --- /dev/null +++ b/src/app/(home)/major-league-balatro/_components/tournament-format.tsx @@ -0,0 +1,107 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Trophy, Users } from 'lucide-react' + +export function TournamentFormat() { + return ( +
+
+

+ Tournament Format +

+

+ Major League Balatro features 12 creators competing in a structured + league format. +

+
+ +
+ + + + + League Structure + + + +
    +
  • + + • 12 creators split into 2 divisions (Blue & Red) + +
  • +
  • + + • Each creator plays 5 matches (one against each division + member) + +
  • +
  • + + • 1st place in each division advances to Final Four + +
  • +
  • + + • 2nd and 3rd place enter a playoff round robin + +
  • +
  • + + • Top 2 from playoff round robin advance to Final Four + +
  • +
  • + + • Finals on Saturday, May 17th, 2025 + +
  • +
+
+
+ + + + + + Match Format + + + +
    +
  • + + • League matches: Best of 3 games + +
  • +
  • + + • Playoff matches: Best of 5 games + +
  • +
  • + + • 1 point per game win + 1 point for match win + +
  • +
  • + + • Alternating deck and stake selection + +
  • +
  • + + • Vanilla ruleset (no multiplayer jokers) + +
  • +
  • + + • PvP battles after Ante-1 where lower score loses a life + +
  • +
+
+
+
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/_constants/matches.ts b/src/app/(home)/major-league-balatro/_constants/matches.ts new file mode 100644 index 0000000..28a85d5 --- /dev/null +++ b/src/app/(home)/major-league-balatro/_constants/matches.ts @@ -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', + // }, +] diff --git a/src/app/(home)/major-league-balatro/_constants/players.ts b/src/app/(home)/major-league-balatro/_constants/players.ts new file mode 100644 index 0000000..a2eae7c --- /dev/null +++ b/src/app/(home)/major-league-balatro/_constants/players.ts @@ -0,0 +1,123 @@ +import type { Player } from '../types' + +export const players: Record = { + 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', + }, + }, +} diff --git a/src/app/(home)/major-league-balatro/page.tsx b/src/app/(home)/major-league-balatro/page.tsx new file mode 100644 index 0000000..78eb10c --- /dev/null +++ b/src/app/(home)/major-league-balatro/page.tsx @@ -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 ( +
+
+ + + + + {/**/} + + + + + + + + + + + + +
+ +
+
+

+ © {new Date().getFullYear()} Major League Balatro. All rights + reserved. +

+
+ + Balatro Multiplayer + + + Documentation + + + About + +
+
+
+
+ ) +} diff --git a/src/app/(home)/major-league-balatro/types.ts b/src/app/(home)/major-league-balatro/types.ts new file mode 100644 index 0000000..0077a4d --- /dev/null +++ b/src/app/(home)/major-league-balatro/types.ts @@ -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 +} diff --git a/src/app/_assets/fonts/m6x11.ttf b/src/app/_assets/fonts/m6x11.ttf new file mode 100644 index 0000000..cb21224 Binary files /dev/null and b/src/app/_assets/fonts/m6x11.ttf differ diff --git a/src/app/layout.config.tsx b/src/app/layout.config.tsx index d759bf6..39dd7a3 100644 --- a/src/app/layout.config.tsx +++ b/src/app/layout.config.tsx @@ -18,6 +18,16 @@ const links = [ url: '/about', icon: , }, + { + type: 'menu' as const, + text: 'Major League Balatro', + items: [ + { + text: 'About', + url: '/major-league-balatro', + }, + ], + }, { text: 'Support Mod Development', url: 'https://ko-fi.com/virtualized/shop', diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e142b4b..5306532 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,7 +8,7 @@ import { NextIntlClientProvider } from 'next-intl' import { getLocale } from 'next-intl/server' import PlausibleProvider from 'next-plausible' import { Geist } from 'next/font/google' - +import localFont from 'next/font/local' export const metadata: Metadata = { title: { template: '%s | Balatro Multiplayer', @@ -24,6 +24,12 @@ const geist = Geist({ variable: '--font-geist-sans', }) +const m6x11 = localFont({ + src: './_assets/fonts/m6x11.ttf', + display: 'swap', + variable: '--font-m6x11', +}) + export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { @@ -31,7 +37,7 @@ export default async function RootLayout({ return ( diff --git a/src/app/stream-card/[id]/_components/stream-card-client.tsx b/src/app/stream-card/[id]/_components/stream-card-client.tsx index c12384c..593d79a 100644 --- a/src/app/stream-card/[id]/_components/stream-card-client.tsx +++ b/src/app/stream-card/[id]/_components/stream-card-client.tsx @@ -50,6 +50,7 @@ function getPlayerData( meaningful_games > 0 ? Math.floor((losses / meaningful_games) * 100) : 0, rank: playerLeaderboardEntry.rank, mmr: Math.round(playerLeaderboardEntry.mmr), + mmrChangeRaw: lastGame?.mmrChange, mmrChange: `${(lastGame?.mmrChange ?? 0) >= 0 ? '+' : ''}${Math.round( lastGame?.mmrChange ?? 0 )}`, @@ -93,8 +94,11 @@ export function StreamCardClient() { const isQueuing = playerState?.status === 'queuing' const opponentId = playerState?.currentMatch?.opponentId return ( -
- +
+ {isQueuing && playerState.queueStartTime && ( )} @@ -160,7 +164,7 @@ function Opponent({ id }: { id: string }) { } const playerData = getPlayerData(rankedUserRank, games) - return + return } function PlayerInfo({ @@ -168,10 +172,12 @@ function PlayerInfo({ className, children, isReverse = false, + isInBattle = false, ...rest }: { playerData: ReturnType isReverse?: boolean + isInBattle?: boolean } & ComponentPropsWithoutRef<'div'>) { return (
MMR:
{playerData.mmr}
-
{playerData.mmrChange}
+
+ {playerData.mmrChange} +
{/* Win Rate */} -
-
Win Rate:
-
- {playerData.winRate}% + {!isInBattle && ( +
+
WR:
+
+ {playerData.winRate}% +
-
+ )} {/* Win/Loss */}
@@ -227,15 +244,17 @@ function PlayerInfo({
{/* Streak */} -
-
Streak:
-
{playerData.streak}
-
+ {!isInBattle && ( +
+
Streak:
+
{playerData.streak}
+
+ )} {children}
diff --git a/src/components/countdown-timer.tsx b/src/components/countdown-timer.tsx new file mode 100644 index 0000000..f6beabc --- /dev/null +++ b/src/components/countdown-timer.tsx @@ -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 ( +
+
+ {[ + { value: timeLeft.days, label: 'Days' }, + { value: timeLeft.hours, label: 'Hours' }, + { value: timeLeft.minutes, label: 'Minutes' }, + { value: timeLeft.seconds, label: 'Seconds' }, + ].map((item, index) => ( +
+
+ + {item.value.toString().padStart(2, '0')} + + + {item.label} + +
+
+ ))} +
+
+ ) +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 71e428b..a4578f2 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -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({ className, @@ -11,9 +11,9 @@ function Avatar({ }: React.ComponentProps) { return ( ) { return ( ) @@ -40,9 +40,9 @@ function AvatarFallback({ }: React.ComponentProps) { return (