development #4

Merged
kyle merged 5 commits from development into main 2026-02-10 14:03:29 -06:00
10 changed files with 181 additions and 85 deletions
Showing only changes of commit 63853f22de - Show all commits

View File

@@ -86,6 +86,7 @@ function RouteComponent() {
<SeedTournament <SeedTournament
tournamentId={tournament.id} tournamentId={tournament.id}
teams={tournament.teams || []} teams={tournament.teams || []}
isRegional={tournament.regional}
/> />
)} )}
</Container> </Container>

View File

@@ -0,0 +1,139 @@
import { Box, AvatarGroup } from "@mantine/core";
import { CrownIcon } from "@phosphor-icons/react";
import { TeamInfo } from "@/features/teams/types";
import Avatar from "./avatar";
import PlayerAvatar from "./player-avatar";
interface TeamAvatarProps {
team: TeamInfo;
size?: number;
radius?: string | number;
withBorder?: boolean;
disableFullscreen?: boolean;
contain?: boolean;
style?: React.CSSProperties;
winner?: boolean;
isRegional?: boolean;
}
const TeamAvatar = ({
team,
size = 35,
radius = "sm",
withBorder = true,
disableFullscreen = false,
contain = false,
style,
winner = false,
isRegional,
}: TeamAvatarProps) => {
const hasNoLogo = !team.logo;
const hasTwoPlayers = team.players?.length === 2;
let shouldShowPlayerAvatars = false;
if (isRegional !== undefined) {
shouldShowPlayerAvatars = isRegional && hasTwoPlayers && hasNoLogo;
} else {
const tournaments = (team as any).tournaments;
const hasTournaments = tournaments && tournaments.length > 0;
const allTournamentsAreRegional = hasTournaments && tournaments.every((t: any) => t.regional === true);
shouldShowPlayerAvatars = hasTwoPlayers && hasNoLogo && (allTournamentsAreRegional || !hasTournaments);
}
if (shouldShowPlayerAvatars && team.players?.length === 2) {
const playerSize = size * 0.6;
const crownSize = Math.max(12, size * 0.35);
return (
<Box
style={{
position: "relative",
width: size,
height: size,
display: "flex",
alignItems: "center",
justifyContent: "center",
...style,
}}
>
<AvatarGroup spacing={size * -0.25}>
<Box style={{ position: "relative" }}>
<PlayerAvatar
name={`${team.players[0].first_name} ${team.players[0].last_name}`}
size={playerSize}
disableFullscreen={disableFullscreen}
/>
{winner && (
<Box
style={{
position: "absolute",
top: -crownSize * 1.1,
left: "45%",
transform: "translateX(-50%)",
color: "gold",
rotate: "-5deg",
}}
>
<CrownIcon size={crownSize} weight="fill" />
</Box>
)}
</Box>
<Box style={{ position: "relative" }}>
<PlayerAvatar
name={`${team.players[1].first_name} ${team.players[1].last_name}`}
size={playerSize}
disableFullscreen={disableFullscreen}
/>
{winner && (
<Box
style={{
position: "absolute",
top: -crownSize * 0.95,
left: "65%",
transform: "translateX(-50%)",
color: "gold",
rotate: "10deg",
}}
>
<CrownIcon size={crownSize} weight="fill" />
</Box>
)}
</Box>
</AvatarGroup>
</Box>
);
}
const crownSize = Math.max(14, size * 0.4);
return (
<Box style={{ position: "relative", ...style }}>
<Avatar
name={team.name}
size={size}
radius={radius}
withBorder={withBorder}
disableFullscreen={disableFullscreen}
contain={contain}
src={team.logo ? `/api/files/teams/${team.id}/${team.logo}` : undefined}
/>
{winner && (
<Box
style={{
position: "absolute",
top: -crownSize * 0.6,
left: -crownSize * 0.25,
transform: "rotate(-25deg)",
color: "gold",
}}
>
<CrownIcon size={crownSize} weight="fill" />
</Box>
)}
</Box>
);
};
export default TeamAvatar;

View File

@@ -1,8 +1,8 @@
import { Text, Group, Stack, Paper, Indicator, Box, Tooltip, ActionIcon } from "@mantine/core"; import { Text, Group, Stack, Paper, Indicator, Box, Tooltip, ActionIcon } from "@mantine/core";
import { CrownIcon, FootballHelmetIcon } from "@phosphor-icons/react"; import { FootballHelmetIcon } from "@phosphor-icons/react";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { Match } from "../types"; import { Match } from "../types";
import Avatar from "@/components/avatar"; import TeamAvatar from "@/components/team-avatar";
import EmojiBar from "@/features/reactions/components/emoji-bar"; import EmojiBar from "@/features/reactions/components/emoji-bar";
import { Suspense } from "react"; import { Suspense } from "react";
import { useSheet } from "@/hooks/use-sheet"; import { useSheet } from "@/hooks/use-sheet";
@@ -113,32 +113,16 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Group gap="sm" style={{ flex: 1 }}> <Group gap="sm" style={{ flex: 1 }}>
<Box <Box
style={{ position: "relative", cursor: "pointer" }} style={{ cursor: "pointer" }}
onClick={handleHomeTeamClick} onClick={handleHomeTeamClick}
> >
<Avatar <TeamAvatar
team={match.home!}
size={40} size={40}
name={match.home?.name!}
radius="sm" radius="sm"
src={ winner={isHomeWin}
match.home?.logo isRegional={match.tournament.regional}
? `/api/files/teams/${match.home?.id}/${match.home?.logo}`
: undefined
}
/> />
{isHomeWin && (
<Box
style={{
position: "absolute",
top: -10,
left: -4,
transform: "rotate(-25deg)",
color: "gold",
}}
>
<CrownIcon size={16} weight="fill" />
</Box>
)}
</Box> </Box>
<Tooltip <Tooltip
label={match.home?.name!} label={match.home?.name!}
@@ -175,32 +159,16 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Group gap="sm" style={{ flex: 1 }}> <Group gap="sm" style={{ flex: 1 }}>
<Box <Box
style={{ position: "relative", cursor: "pointer" }} style={{ cursor: "pointer" }}
onClick={handleAwayTeamClick} onClick={handleAwayTeamClick}
> >
<Avatar <TeamAvatar
team={match.away!}
size={40} size={40}
name={match.away?.name!}
radius="sm" radius="sm"
src={ winner={isAwayWin}
match.away?.logo isRegional={match.tournament.regional}
? `/api/files/teams/${match.away?.id}/${match.away?.logo}`
: undefined
}
/> />
{isAwayWin && (
<Box
style={{
position: "absolute",
top: -10,
left: -4,
transform: "rotate(-25deg)",
color: "gold",
}}
>
<CrownIcon size={16} weight="fill" />
</Box>
)}
</Box> </Box>
<Tooltip <Tooltip
label={match.away?.name} label={match.away?.name}

View File

@@ -8,7 +8,7 @@ import {
Title Title
} from "@mantine/core"; } from "@mantine/core";
import { useTeam } from "../queries"; import { useTeam } from "../queries";
import Avatar from "@/components/avatar"; import TeamAvatar from "@/components/team-avatar";
import SongSummary from "./team-form/song-summary"; import SongSummary from "./team-form/song-summary";
interface TeamCardProps { interface TeamCardProps {
@@ -46,11 +46,10 @@ const TeamCard = ({ teamId }: TeamCardProps) => {
> >
<Stack gap={2}> <Stack gap={2}>
<Group gap="md" align="center" p="xs"> <Group gap="md" align="center" p="xs">
<Avatar <TeamAvatar
name={team.name} team={team}
size={40} size={40}
radius="md" radius="md"
src={team.logo ? `/api/files/teams/${team.id}/${team.logo}` : undefined}
style={{ style={{
backgroundColor: team.primary_color || undefined, backgroundColor: team.primary_color || undefined,
color: team.accent_color || undefined, color: team.accent_color || undefined,

View File

@@ -7,7 +7,7 @@ import {
Stack, Stack,
Text, Text,
} from "@mantine/core"; } from "@mantine/core";
import Avatar from "@/components/avatar"; import TeamAvatar from "@/components/team-avatar";
import { TeamInfo } from "@/features/teams/types"; import { TeamInfo } from "@/features/teams/types";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
@@ -88,15 +88,10 @@ const TeamList = ({ teams, loading = false, onTeamClick }: TeamListProps) => {
key={`team-list-${team.id}`} key={`team-list-${team.id}`}
p="xs" p="xs"
icon={ icon={
<Avatar <TeamAvatar
team={team}
radius="sm" radius="sm"
size={40} size={40}
name={`${team.name}`}
src={
team.logo
? `/api/files/teams/${team.id}/${team.logo}`
: undefined
}
/> />
} }
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}

View File

@@ -11,7 +11,7 @@ import { useState, useCallback, useMemo, memo } from "react";
import { useTournament, useUnenrolledTeams } from "../queries"; import { useTournament, useUnenrolledTeams } from "../queries";
import useEnrollTeam from "../hooks/use-enroll-team"; import useEnrollTeam from "../hooks/use-enroll-team";
import useUnenrollTeam from "../hooks/use-unenroll-team"; import useUnenrollTeam from "../hooks/use-unenroll-team";
import Avatar from "@/components/avatar"; import TeamAvatar from "@/components/team-avatar";
import { Team, TeamInfo } from "@/features/teams/types"; import { Team, TeamInfo } from "@/features/teams/types";
interface EditEnrolledTeamsProps { interface EditEnrolledTeamsProps {
@@ -22,9 +22,10 @@ interface TeamItemProps {
team: TeamInfo; team: TeamInfo;
onUnenroll: (teamId: string) => void; onUnenroll: (teamId: string) => void;
disabled: boolean; disabled: boolean;
isRegional?: boolean;
} }
const TeamItem = memo(({ team, onUnenroll, disabled }: TeamItemProps) => { const TeamItem = memo(({ team, onUnenroll, disabled, isRegional }: TeamItemProps) => {
const playerNames = useMemo( const playerNames = useMemo(
() => () =>
team.players?.map((p) => `${p.first_name} ${p.last_name}`).join(", ") || team.players?.map((p) => `${p.first_name} ${p.last_name}`).join(", ") ||
@@ -34,15 +35,11 @@ const TeamItem = memo(({ team, onUnenroll, disabled }: TeamItemProps) => {
return ( return (
<Group py="xs" px="sm" w="100%" gap="sm" align="center"> <Group py="xs" px="sm" w="100%" gap="sm" align="center">
<Avatar <TeamAvatar
team={team}
size={32} size={32}
radius="sm" radius="sm"
name={team.name} isRegional={isRegional}
src={
team.logo
? `/api/files/teams/${team.id}/${team.logo}`
: undefined
}
/> />
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}> <Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} truncate> <Text fw={500} truncate>
@@ -73,6 +70,8 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
const { data: unenrolledTeams = [], isLoading: unenrolledLoading } = const { data: unenrolledTeams = [], isLoading: unenrolledLoading } =
useUnenrolledTeams(tournamentId); useUnenrolledTeams(tournamentId);
const isRegional = tournament?.regional;
const { mutate: enrollTeam, isPending: isEnrolling } = useEnrollTeam(); const { mutate: enrollTeam, isPending: isEnrolling } = useEnrollTeam();
const { mutate: unenrollTeam, isPending: isUnenrolling } = useUnenrollTeam(); const { mutate: unenrollTeam, isPending: isUnenrolling } = useUnenrollTeam();
@@ -107,15 +106,11 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
const team = option.data; const team = option.data;
return ( return (
<Group py="xs" px="sm" gap="sm" align="center"> <Group py="xs" px="sm" gap="sm" align="center">
<Avatar <TeamAvatar
team={team as any}
size={32} size={32}
radius="sm" radius="sm"
name={team.name} isRegional={isRegional}
src={
team.logo
? `/api/files/teams/${team.id}/${team.logo}`
: undefined
}
/> />
<Text fw={500} truncate> <Text fw={500} truncate>
{team.name} {team.name}
@@ -174,6 +169,7 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
team={team} team={team}
onUnenroll={handleUnenrollTeam} onUnenroll={handleUnenrollTeam}
disabled={isUnenrolling} disabled={isUnenrolling}
isRegional={isRegional}
/> />
))} ))}
</Stack> </Stack>

View File

@@ -13,7 +13,7 @@ import { DotsNineIcon } from "@phosphor-icons/react";
import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation"; import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation";
import { generateTournamentBracket } from "../../matches/server"; import { generateTournamentBracket } from "../../matches/server";
import { TeamInfo } from "@/features/teams/types"; import { TeamInfo } from "@/features/teams/types";
import Avatar from "@/components/avatar"; import TeamAvatar from "@/components/team-avatar";
import { useBracketPreview } from "@/features/bracket/queries"; import { useBracketPreview } from "@/features/bracket/queries";
import { BracketData } from "@/features/bracket/types"; import { BracketData } from "@/features/bracket/types";
import BracketView from "@/features/bracket/components/bracket-view"; import BracketView from "@/features/bracket/components/bracket-view";
@@ -23,11 +23,13 @@ import { tournamentKeys } from "../queries";
interface SeedTournamentProps { interface SeedTournamentProps {
tournamentId: string; tournamentId: string;
teams: TeamInfo[]; teams: TeamInfo[];
isRegional?: boolean;
} }
const SeedTournament: React.FC<SeedTournamentProps> = ({ const SeedTournament: React.FC<SeedTournamentProps> = ({
tournamentId, tournamentId,
teams, teams,
isRegional,
}) => { }) => {
const [orderedTeams, setOrderedTeams] = useState<TeamInfo[]>(teams); const [orderedTeams, setOrderedTeams] = useState<TeamInfo[]>(teams);
const { data: bracketPreview } = useBracketPreview(teams.length); const { data: bracketPreview } = useBracketPreview(teams.length);
@@ -171,15 +173,11 @@ const SeedTournament: React.FC<SeedTournamentProps> = ({
}} }}
/> />
<Avatar <TeamAvatar
team={team}
size={24} size={24}
radius="sm" radius="sm"
name={team.name} isRegional={isRegional}
src={
team.logo
? `/api/files/teams/${team.id}/${team.logo}`
: undefined
}
/> />
<Text fw={500} size="sm" style={{ flex: 1 }}> <Text fw={500} size="sm" style={{ flex: 1 }}>

View File

@@ -7,7 +7,7 @@ export function createMatchesService(pb: PocketBase) {
return { return {
async getMatch(id: string): Promise<Match | null> { async getMatch(id: string): Promise<Match | null> {
const result = await pb.collection("matches").getOne(id, { const result = await pb.collection("matches").getOne(id, {
expand: "tournament, home, away", expand: "tournament, home, away, home.players, away.players",
}); });
return transformMatch(result); return transformMatch(result);
}, },
@@ -19,7 +19,7 @@ export function createMatchesService(pb: PocketBase) {
const result = await pb.collection("matches").getFullList({ const result = await pb.collection("matches").getFullList({
filter: `tournament="${match.tournament.id}" && (home_from_lid = ${match.lid} || away_from_lid = ${match.lid}) && bye = false`, filter: `tournament="${match.tournament.id}" && (home_from_lid = ${match.lid} || away_from_lid = ${match.lid}) && bye = false`,
expand: "tournament, home, away", expand: "tournament, home, away, home.players, away.players",
}); });
const winnerMatch = result.find(m => (m.home_from_lid === match.lid && !m.home_from_loser) || (m.away_from_lid === match.lid && !m.away_from_loser)); const winnerMatch = result.find(m => (m.home_from_lid === match.lid && !m.home_from_loser) || (m.away_from_lid === match.lid && !m.away_from_loser));
@@ -50,7 +50,7 @@ export function createMatchesService(pb: PocketBase) {
async updateMatch(id: string, data: Partial<MatchInput>): Promise<Match> { async updateMatch(id: string, data: Partial<MatchInput>): Promise<Match> {
logger.info("PocketBase | Updating match", { id, data }); logger.info("PocketBase | Updating match", { id, data });
const result = await pb.collection("matches").update<Match>(id, data, { const result = await pb.collection("matches").update<Match>(id, data, {
expand: 'home, away, tournament' expand: 'home, away, tournament, home.players, away.players'
}); });
return transformMatch(result); return transformMatch(result);
}, },

View File

@@ -126,7 +126,7 @@ export function createPlayersService(pb: PocketBase) {
const result = await pb.collection("matches").getFullList({ const result = await pb.collection("matches").getFullList({
filter: `(${teamFilter}) && (status = "ended" || status = "started")`, filter: `(${teamFilter}) && (status = "ended" || status = "started")`,
sort: "-created", sort: "-created",
expand: "tournament,home,away", expand: "tournament,home,away,home.players,away.players",
}); });
return result.map((match) => transformMatch(match)); return result.map((match) => transformMatch(match));

View File

@@ -105,7 +105,7 @@ export function createTeamsService(pb: PocketBase) {
const result = await pb.collection("matches").getFullList({ const result = await pb.collection("matches").getFullList({
filter: `(${teamFilter}) && (status = "ended" || status = "started")`, filter: `(${teamFilter}) && (status = "ended" || status = "started")`,
sort: "-start_time", sort: "-start_time",
expand: "tournament,home,away", expand: "tournament,home,away,home.players,away.players",
}); });
return result.map((match) => transformMatch(match)); return result.map((match) => transformMatch(match));