award badges
This commit is contained in:
@@ -33,6 +33,7 @@ import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_aut
|
||||
import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId'
|
||||
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
|
||||
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
|
||||
import { Route as AuthedAdminBadgesRouteImport } from './routes/_authed/admin/badges'
|
||||
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
||||
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
|
||||
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
|
||||
@@ -161,6 +162,11 @@ const AuthedAdminPreviewRoute = AuthedAdminPreviewRouteImport.update({
|
||||
path: '/preview',
|
||||
getParentRoute: () => AuthedAdminRoute,
|
||||
} as any)
|
||||
const AuthedAdminBadgesRoute = AuthedAdminBadgesRouteImport.update({
|
||||
id: '/badges',
|
||||
path: '/badges',
|
||||
getParentRoute: () => AuthedAdminRoute,
|
||||
} as any)
|
||||
const AuthedAdminTournamentsIndexRoute =
|
||||
AuthedAdminTournamentsIndexRouteImport.update({
|
||||
id: '/tournaments/',
|
||||
@@ -206,6 +212,7 @@ export interface FileRoutesByFullPath {
|
||||
'/settings': typeof AuthedSettingsRoute
|
||||
'/stats': typeof AuthedStatsRoute
|
||||
'/': typeof AuthedIndexRoute
|
||||
'/admin/badges': typeof AuthedAdminBadgesRoute
|
||||
'/admin/preview': typeof AuthedAdminPreviewRoute
|
||||
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||
@@ -236,6 +243,7 @@ export interface FileRoutesByTo {
|
||||
'/settings': typeof AuthedSettingsRoute
|
||||
'/stats': typeof AuthedStatsRoute
|
||||
'/': typeof AuthedIndexRoute
|
||||
'/admin/badges': typeof AuthedAdminBadgesRoute
|
||||
'/admin/preview': typeof AuthedAdminPreviewRoute
|
||||
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||
@@ -269,6 +277,7 @@ export interface FileRoutesById {
|
||||
'/_authed/settings': typeof AuthedSettingsRoute
|
||||
'/_authed/stats': typeof AuthedStatsRoute
|
||||
'/_authed/': typeof AuthedIndexRoute
|
||||
'/_authed/admin/badges': typeof AuthedAdminBadgesRoute
|
||||
'/_authed/admin/preview': typeof AuthedAdminPreviewRoute
|
||||
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||
'/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||
@@ -302,6 +311,7 @@ export interface FileRouteTypes {
|
||||
| '/settings'
|
||||
| '/stats'
|
||||
| '/'
|
||||
| '/admin/badges'
|
||||
| '/admin/preview'
|
||||
| '/profile/$playerId'
|
||||
| '/teams/$teamId'
|
||||
@@ -332,6 +342,7 @@ export interface FileRouteTypes {
|
||||
| '/settings'
|
||||
| '/stats'
|
||||
| '/'
|
||||
| '/admin/badges'
|
||||
| '/admin/preview'
|
||||
| '/profile/$playerId'
|
||||
| '/teams/$teamId'
|
||||
@@ -364,6 +375,7 @@ export interface FileRouteTypes {
|
||||
| '/_authed/settings'
|
||||
| '/_authed/stats'
|
||||
| '/_authed/'
|
||||
| '/_authed/admin/badges'
|
||||
| '/_authed/admin/preview'
|
||||
| '/_authed/profile/$playerId'
|
||||
| '/_authed/teams/$teamId'
|
||||
@@ -576,6 +588,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthedAdminPreviewRouteImport
|
||||
parentRoute: typeof AuthedAdminRoute
|
||||
}
|
||||
'/_authed/admin/badges': {
|
||||
id: '/_authed/admin/badges'
|
||||
path: '/badges'
|
||||
fullPath: '/admin/badges'
|
||||
preLoaderRoute: typeof AuthedAdminBadgesRouteImport
|
||||
parentRoute: typeof AuthedAdminRoute
|
||||
}
|
||||
'/_authed/admin/tournaments/': {
|
||||
id: '/_authed/admin/tournaments/'
|
||||
path: '/tournaments'
|
||||
@@ -622,6 +641,7 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
|
||||
interface AuthedAdminRouteChildren {
|
||||
AuthedAdminBadgesRoute: typeof AuthedAdminBadgesRoute
|
||||
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
||||
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
||||
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
||||
@@ -631,6 +651,7 @@ interface AuthedAdminRouteChildren {
|
||||
}
|
||||
|
||||
const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
||||
AuthedAdminBadgesRoute: AuthedAdminBadgesRoute,
|
||||
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
||||
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
||||
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
||||
|
||||
10
src/app/routes/_authed/admin/badges.tsx
Normal file
10
src/app/routes/_authed/admin/badges.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import AwardBadges from "@/features/admin/components/award-badges";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/badges")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <AwardBadges />;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
99
src/features/admin/components/award-badges.tsx
Normal file
99
src/features/admin/components/award-badges.tsx
Normal 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;
|
||||
@@ -86,7 +86,7 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mb="lg">
|
||||
<Box px="md" mb="lg">
|
||||
<Card
|
||||
withBorder
|
||||
radius="md"
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
|
||||
@@ -351,6 +351,32 @@ export function createBadgesService(pb: PocketBase) {
|
||||
);
|
||||
},
|
||||
|
||||
async awardManualBadge(playerId: string, badgeId: string): Promise<BadgeProgress> {
|
||||
// Get or create badge progress record
|
||||
const existingProgress = await pb.collection("badge_progress").getFirstListItem(
|
||||
`player = "${playerId}" && badge = "${badgeId}"`,
|
||||
{ expand: 'badge' }
|
||||
).catch(() => null);
|
||||
|
||||
if (existingProgress) {
|
||||
// Update existing progress to mark as earned
|
||||
const updated = await pb.collection("badge_progress").update(existingProgress.id, {
|
||||
progress: 1,
|
||||
earned: true,
|
||||
}, { expand: 'badge' });
|
||||
return transformBadgeProgress(updated);
|
||||
}
|
||||
|
||||
// Create new progress record
|
||||
const created = await pb.collection("badge_progress").create({
|
||||
badge: badgeId,
|
||||
player: playerId,
|
||||
progress: 1,
|
||||
earned: true,
|
||||
}, { expand: 'badge' });
|
||||
return transformBadgeProgress(created);
|
||||
},
|
||||
|
||||
async migrateBadgeProgress(): Promise<{
|
||||
success: boolean;
|
||||
playersProcessed: number;
|
||||
|
||||
Reference in New Issue
Block a user