- dock h2h source corrected to match_dock; badge_viewed breakdown via value - stats filter guards unified, ref-based, double-tap safe - emoji picker reuses handleReactionClick; comment and import tidy
194 lines
5.6 KiB
TypeScript
194 lines
5.6 KiB
TypeScript
import {
|
|
Stack,
|
|
Container,
|
|
Title,
|
|
Box,
|
|
Divider,
|
|
Text,
|
|
Grid,
|
|
UnstyledButton,
|
|
} from '@mantine/core';
|
|
import { useAllBadges, useAllEarnedBadges } from '../queries';
|
|
import { BadgeIcon } from './badge-showcase';
|
|
import { useMemo } from 'react';
|
|
import { Badge, EarnedBadge } from '../types';
|
|
import { useAllPlayers } from '@/features/teams/hooks/use-available-players';
|
|
import { useSheet } from '@/hooks/use-sheet';
|
|
import Sheet from '@/components/sheet/sheet';
|
|
import PlayerList from '@/features/players/components/player-list';
|
|
import { useAuth } from '@/contexts/auth-context';
|
|
import { Player } from '@/features/players/types';
|
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
import { track } from '@/lib/telemetry/client';
|
|
|
|
const BadgeStatsTable = () => {
|
|
const { data: allBadges } = useAllBadges();
|
|
const { data: allEarnedBadges } = useAllEarnedBadges();
|
|
const { data: allPlayers } = useAllPlayers();
|
|
const totalNumPlayers = allPlayers?.length || 0;
|
|
|
|
const groupedEarnedBadges = useMemo(() => {
|
|
const returnDict = new Map<string, EarnedBadge[]>();
|
|
allEarnedBadges?.forEach((earnedBadge) => {
|
|
if (!returnDict.has(earnedBadge.badge)) {
|
|
returnDict.set(earnedBadge.badge, []);
|
|
}
|
|
returnDict.get(earnedBadge.badge)!.push(earnedBadge);
|
|
});
|
|
return returnDict;
|
|
}, [allEarnedBadges]);
|
|
|
|
if (allBadges.length === 0) {
|
|
return (
|
|
<Container px={0} size='md'>
|
|
<Stack align='center' gap='md' py='xl'>
|
|
<Title order={3} c='dimmed'>
|
|
<Trans>No Badges Available</Trans>
|
|
</Title>
|
|
</Stack>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Container size='100%' px={0}>
|
|
<Stack gap='xs'>
|
|
<Stack gap={0}>
|
|
{allBadges.map((badge, index) => (
|
|
<BadgeStatRow
|
|
badge={badge}
|
|
totalNumPlayers={totalNumPlayers}
|
|
earnedBadges={groupedEarnedBadges.get(badge.id) || []}
|
|
isLastRow={index >= allBadges.length - 1}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
</Stack>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default BadgeStatsTable;
|
|
|
|
interface BadgeStatRowProps {
|
|
badge: Badge;
|
|
totalNumPlayers: number;
|
|
earnedBadges: EarnedBadge[];
|
|
isLastRow: boolean;
|
|
}
|
|
|
|
const BadgeStatRow: React.FC<BadgeStatRowProps> = ({
|
|
badge,
|
|
totalNumPlayers,
|
|
earnedBadges,
|
|
isLastRow,
|
|
}) => {
|
|
const badgeSheet = useSheet();
|
|
const { user } = useAuth();
|
|
const { t } = useLingui();
|
|
|
|
const playerNamesBlurb = useMemo(() => {
|
|
if (earnedBadges.length === 0) return t`No players yet`;
|
|
|
|
const currentUserHasBadge = earnedBadges.some(
|
|
(eb) => eb.player.id === user?.id
|
|
);
|
|
|
|
const otherPlayers = earnedBadges.filter(
|
|
(eb) => eb.player.id !== user?.id
|
|
);
|
|
|
|
const displayPlayers = currentUserHasBadge
|
|
? otherPlayers.slice(0, 2)
|
|
: earnedBadges.slice(0, 3);
|
|
|
|
const names = displayPlayers.map((eb) => eb.player.first_name);
|
|
const namesList = names.join(', ');
|
|
|
|
if (currentUserHasBadge) {
|
|
const remaining = earnedBadges.length - 1 - names.length;
|
|
if (names.length === 0 && remaining === 0) {
|
|
return t`You`;
|
|
} else if (names.length === 0 && remaining > 0) {
|
|
return remaining > 1
|
|
? t`You and ${remaining} others`
|
|
: t`You and 1 other`;
|
|
} else if (remaining > 0) {
|
|
return remaining > 1
|
|
? t`You, ${namesList} and ${remaining} others`
|
|
: t`You, ${namesList} and 1 other`;
|
|
} else {
|
|
return names.length > 0 ? t`You and ${namesList}` : t`You`;
|
|
}
|
|
} else {
|
|
const remaining = earnedBadges.length - names.length;
|
|
if (remaining > 0) {
|
|
return remaining > 1
|
|
? t`${namesList} and ${remaining} others`
|
|
: t`${namesList} and 1 other`;
|
|
} else {
|
|
return namesList;
|
|
}
|
|
}
|
|
}, [earnedBadges, user?.id, t]);
|
|
|
|
const playersForList: Player[] = useMemo(() => {
|
|
return earnedBadges.map((eb) => ({
|
|
id: eb.player.id,
|
|
first_name: eb.player.first_name,
|
|
last_name: eb.player.last_name,
|
|
} as Player));
|
|
}, [earnedBadges]);
|
|
|
|
return (
|
|
<Box key={badge.id}>
|
|
<UnstyledButton
|
|
onClick={() => {
|
|
track("badge_viewed", { value: badge.key });
|
|
badgeSheet.open();
|
|
}}
|
|
w='100%'
|
|
>
|
|
<Grid p={'xs'} align='center'>
|
|
<Grid.Col span={2} style={{ display: 'flex', alignItems: 'center' }}>
|
|
<BadgeIcon badge={badge} filled={true} />
|
|
</Grid.Col>
|
|
<Grid.Col span={'auto'}>
|
|
<Stack gap={2}>
|
|
<Text fw={700}>{badge.name}</Text>
|
|
<Text size="sm" fw={500}>{badge.description}</Text>
|
|
<Text size='xs' c='dimmed' mt={4}>
|
|
{playerNamesBlurb}
|
|
</Text>
|
|
</Stack>
|
|
</Grid.Col>
|
|
<Grid.Col
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-end',
|
|
width: '73px',
|
|
flexBasis: '73px', // ensures the width is respected
|
|
}}
|
|
>
|
|
<Stack gap={0} align='center'>
|
|
<Text c="dimmed" size='lg' fw={700}>
|
|
{(
|
|
((earnedBadges?.length ?? 0) / totalNumPlayers) *
|
|
100
|
|
).toFixed(0)}
|
|
%
|
|
</Text>
|
|
<Text c="dimmed" size='xs'><Trans>of players</Trans></Text>
|
|
</Stack>
|
|
</Grid.Col>
|
|
</Grid>
|
|
</UnstyledButton>
|
|
<Sheet title={t`${badge.name} Badge Holders`} {...badgeSheet.props}>
|
|
<PlayerList players={playersForList} />
|
|
</Sheet>
|
|
{!isLastRow && <Divider />}
|
|
</Box>
|
|
);
|
|
};
|