Files
flxn-app/src/features/badges/components/badge-stats-table.tsx
T
kyle 8e42862d20
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / i18n Catalog Check (push) Failing after 49s
CI/CD Pipeline / Build and Push App Docker Image (push) Skipped
CI/CD Pipeline / Deploy to Kubernetes (push) Skipped
fix(telemetry): review nits
- 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
2026-08-26 23:19:45 -07:00

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>
);
};