award badges

This commit is contained in:
yohlo
2025-10-01 13:42:09 -05:00
parent 654041b6b6
commit 6224404aa9
7 changed files with 173 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ import {
TreeStructureIcon,
TrophyIcon,
MedalIcon,
CrownIcon,
} from "@phosphor-icons/react";
import ListButton from "@/components/list-button";
import { migrateBadgeProgress } from "@/features/badges/server";
@@ -28,6 +29,11 @@ const AdminPage = () => {
Icon={TrophyIcon}
to="/admin/tournaments"
/>
<ListLink
label="Award Badges"
Icon={CrownIcon}
to="/admin/badges"
/>
<ListButton
label="Migrate Badge Progress"
Icon={MedalIcon}

View File

@@ -0,0 +1,99 @@
import { useState } from "react";
import { Box, Card, Text, Select, Button, Group, Stack } from "@mantine/core";
import { awardManualBadge } from "@/features/badges/server";
import { useAllBadges } from "@/features/badges/queries";
import toast from "@/lib/sonner";
import { usePlayers } from "@/features/players/queries";
const AwardBadges = () => {
const { data: players } = usePlayers();
const { data: allBadges } = useAllBadges();
const [selectedPlayerId, setSelectedPlayerId] = useState<string | null>(null);
const [selectedBadgeId, setSelectedBadgeId] = useState<string | null>(null);
const [isAwarding, setIsAwarding] = useState(false);
const manualBadges = allBadges.filter((badge) => badge.type === "manual");
const handleAwardBadge = async () => {
if (!selectedPlayerId || !selectedBadgeId) return;
setIsAwarding(true);
try {
await awardManualBadge({
data: {
playerId: selectedPlayerId,
badgeId: selectedBadgeId,
},
});
toast.success("Badge awarded successfully");
setSelectedPlayerId(null);
setSelectedBadgeId(null);
} catch (error) {
toast.error("Failed to award badge");
} finally {
setIsAwarding(false);
}
};
const playerOptions = players.map((player) => ({
value: player.id,
label: `${player.first_name} ${player.last_name}`,
}));
const badgeOptions = manualBadges.map((badge) => ({
value: badge.id,
label: badge.name,
}));
return (
<Box p="md">
<Card withBorder radius="md" p="md">
<Stack gap="md">
<Box>
<Text size="lg" fw={600} mb="xs">
Award Manual Badge
</Text>
<Text size="sm" c="dimmed">
Select a player and a manual badge to award
</Text>
</Box>
<Select
label="Player"
placeholder="Select a player"
data={playerOptions}
value={selectedPlayerId}
onChange={setSelectedPlayerId}
searchable
clearable
/>
<Select
label="Badge"
placeholder="Select a badge"
data={badgeOptions}
value={selectedBadgeId}
onChange={setSelectedBadgeId}
searchable
clearable
/>
<Group justify="flex-end">
<Button
onClick={handleAwardBadge}
disabled={!selectedPlayerId || !selectedBadgeId}
loading={isAwarding}
>
Award Badge
</Button>
</Group>
</Stack>
</Card>
</Box>
);
};
export default AwardBadges;

View File

@@ -86,7 +86,7 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
}
return (
<Box mb="lg">
<Box px="md" mb="lg">
<Card
withBorder
radius="md"

View File

@@ -22,3 +22,13 @@ export const getAllBadges = createServerFn()
.handler(async () =>
toServerResult(() => pbAdmin.listBadges())
);
export const awardManualBadge = createServerFn()
.inputValidator(z.object({
playerId: z.string(),
badgeId: z.string(),
}))
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult(() => pbAdmin.awardManualBadge(data.playerId, data.badgeId))
);