working but sheet styling is ugly
This commit is contained in:
@@ -18,15 +18,15 @@ interface BadgeDisplay {
|
||||
|
||||
interface BadgeIconProps {
|
||||
badge: Badge;
|
||||
earned: boolean;
|
||||
filled: boolean;
|
||||
}
|
||||
|
||||
const BadgeIcon = ({ badge, earned, size = 48 }: BadgeIconProps & { size?: number }) => {
|
||||
export const BadgeIcon = ({ badge, filled, size = 48 }: BadgeIconProps & { size?: number }) => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const imagePath = `/static/img/${badge.key}.png`;
|
||||
|
||||
if (imageError) {
|
||||
return earned ? (
|
||||
return filled ? (
|
||||
<MedalIcon
|
||||
size={size}
|
||||
weight="fill"
|
||||
@@ -50,7 +50,7 @@ const BadgeIcon = ({ badge, earned, size = 48 }: BadgeIconProps & { size?: numbe
|
||||
onError={() => setImageError(true)}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
opacity: earned ? 1 : 0.4,
|
||||
opacity: filled ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -218,7 +218,7 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<BadgeIcon badge={display.badge} earned={display.earned} />
|
||||
<BadgeIcon badge={display.badge} filled={display.earned} />
|
||||
|
||||
{showStack && (
|
||||
<Box
|
||||
@@ -256,7 +256,7 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
<Popover.Dropdown>
|
||||
<Stack gap={4} align="center">
|
||||
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<BadgeIcon badge={display.badge} earned={display.earned} size={80} />
|
||||
<BadgeIcon badge={display.badge} filled={display.earned} size={80} />
|
||||
</Box>
|
||||
|
||||
<Title order={5} ta="center">
|
||||
|
||||
129
src/features/badges/components/badge-stats-table.tsx
Normal file
129
src/features/badges/components/badge-stats-table.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
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';
|
||||
|
||||
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'>
|
||||
No Badges Available
|
||||
</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();
|
||||
return (
|
||||
<Box key={badge.id}>
|
||||
<UnstyledButton onClick={badgeSheet.open} w='100%'>
|
||||
<Grid p={'xs'}>
|
||||
<Grid.Col span={2}>
|
||||
<BadgeIcon badge={badge} filled={true} />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={'auto'}>
|
||||
<Text fw={700}>{badge.name}</Text>
|
||||
<Text fw={500}>{badge.description}</Text>
|
||||
</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 size='lg' fw={700}>
|
||||
{(
|
||||
((earnedBadges?.length ?? 0) / totalNumPlayers) *
|
||||
100
|
||||
).toFixed(0)}
|
||||
%
|
||||
</Text>
|
||||
<Text size='xs'>of players</Text>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</UnstyledButton>
|
||||
<Sheet title={badge.name + ' Badge Holders'} {...badgeSheet.props}>
|
||||
<Box mx='xl'>
|
||||
{earnedBadges?.map((earnedBadge) => (
|
||||
<Text>
|
||||
{earnedBadge.player.first_name +
|
||||
' ' +
|
||||
earnedBadge.player.last_name}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Sheet>
|
||||
{!isLastRow && <Divider />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||
import { getPlayerBadges, getAllBadges } from "./server";
|
||||
import { getPlayerBadges, getAllBadges, getAllEarnedBadges } from "./server";
|
||||
|
||||
export const badgeKeys = {
|
||||
playerBadges: (playerId: string) => ['badges', 'player', playerId],
|
||||
allBadges: () => ['badges', 'all'],
|
||||
allEarnedBadges: () => ['badges', 'earned'],
|
||||
};
|
||||
|
||||
export const badgeQueries = {
|
||||
@@ -15,6 +16,10 @@ export const badgeQueries = {
|
||||
queryKey: badgeKeys.allBadges(),
|
||||
queryFn: async () => await getAllBadges()
|
||||
}),
|
||||
allEarnedBadges: () => ({
|
||||
queryKey: badgeKeys.allEarnedBadges(),
|
||||
queryFn: async () => await getAllEarnedBadges(),
|
||||
}),
|
||||
};
|
||||
|
||||
export const usePlayerBadges = (playerId: string) =>
|
||||
@@ -22,3 +27,6 @@ export const usePlayerBadges = (playerId: string) =>
|
||||
|
||||
export const useAllBadges = () =>
|
||||
useServerSuspenseQuery(badgeQueries.allBadges());
|
||||
|
||||
export const useAllEarnedBadges = () =>
|
||||
useServerSuspenseQuery(badgeQueries.allEarnedBadges());
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||
import { superTokensAdminFunctionMiddleware, superTokensFunctionMiddleware } from "@/utils/supertokens";
|
||||
import {
|
||||
superTokensAdminFunctionMiddleware,
|
||||
superTokensFunctionMiddleware,
|
||||
} from "@/utils/supertokens";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { z } from "zod";
|
||||
@@ -19,14 +22,17 @@ export const migrateBadgeProgress = createServerFn()
|
||||
|
||||
export const getAllBadges = createServerFn()
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async () =>
|
||||
toServerResult(() => pbAdmin.listBadges())
|
||||
);
|
||||
.handler(async () =>
|
||||
toServerResult(() => pbAdmin.listBadges()));
|
||||
|
||||
export const getAllEarnedBadges = createServerFn()
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async () => toServerResult(() => pbAdmin.listEarnedBadges()));
|
||||
|
||||
export const awardManualBadge = createServerFn()
|
||||
.inputValidator(z.object({
|
||||
playerId: z.string(),
|
||||
badgeId: z.string(),
|
||||
playerId: z.string(),
|
||||
badgeId: z.string(),
|
||||
}))
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { PlayerInfo } from '../players/types';
|
||||
|
||||
export interface BadgeInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -23,3 +25,13 @@ export interface BadgeProgress {
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface EarnedBadge {
|
||||
id: string;
|
||||
badge: string;
|
||||
player: PlayerInfo;
|
||||
progress: number;
|
||||
earned: boolean;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HouseIcon, RankingIcon, ShieldIcon, TrophyIcon, UserCircleIcon } from "@phosphor-icons/react";
|
||||
import { HouseIcon, RankingIcon, SealIcon, ShieldIcon, TrophyIcon, UserCircleIcon } from "@phosphor-icons/react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useLinks = (userId: string | undefined, roles: string[]) =>
|
||||
@@ -25,6 +25,11 @@ export const useLinks = (userId: string | undefined, roles: string[]) =>
|
||||
href: `/profile/${userId}`,
|
||||
Icon: UserCircleIcon,
|
||||
include: ['/settings']
|
||||
},
|
||||
{
|
||||
label: 'Badges',
|
||||
href: '/badges',
|
||||
Icon: SealIcon
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -16,22 +16,22 @@ export const playerQueries = {
|
||||
|
||||
export const useAvailablePlayers = (excludedPlayerIds: string[] = []) => {
|
||||
const { data: allPlayers } = useServerSuspenseQuery(playerQueries.all());
|
||||
|
||||
|
||||
const availablePlayers = useMemo(() => {
|
||||
if (!allPlayers) return [];
|
||||
|
||||
|
||||
return allPlayers.filter(player =>
|
||||
!excludedPlayerIds.includes(player.id) &&
|
||||
player.first_name &&
|
||||
player.last_name
|
||||
!excludedPlayerIds.includes(player.id) &&
|
||||
player.first_name &&
|
||||
player.last_name
|
||||
);
|
||||
}, [allPlayers, excludedPlayerIds]);
|
||||
|
||||
const playerOptions = useMemo(() =>
|
||||
availablePlayers.map(player => ({
|
||||
value: player.id,
|
||||
value: player.id,
|
||||
label: `${player.first_name} ${player.last_name}`.trim() || 'Unnamed Player'
|
||||
})),
|
||||
})),
|
||||
[availablePlayers]
|
||||
);
|
||||
|
||||
@@ -40,4 +40,6 @@ export const useAvailablePlayers = (excludedPlayerIds: string[] = []) => {
|
||||
playerOptions,
|
||||
allPlayers
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const useAllPlayers = () => useServerSuspenseQuery(playerQueries.all());
|
||||
|
||||
Reference in New Issue
Block a user