Badge images!
BIN
public/static/img/bronze_medal_badge.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/static/img/developer_badge.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
public/static/img/dunce_cap_badge.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/static/img/experienced_player_badge.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/static/img/getting_started_badge.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/static/img/helper_badge.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
public/static/img/hoster_badge.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 747 KiB |
BIN
public/static/img/new_player_badge.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
public/static/img/out_of_towner_badge.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
public/static/img/pilot_program_badge.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
public/static/img/regional_winner_badge.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/static/img/regular_player_badge.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/static/img/reigning_champion_badge.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
public/static/img/runners_club_badge.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
public/static/img/silver_medal_badge.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/static/img/veteran_1_badge.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/static/img/veteran_2_badge.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/static/img/veteran_3_badge.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
public/static/img/veteran_4_badge.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
public/static/img/veteran_5_badge.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
public/static/img/veteran_6_badge.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/static/img/veteran_7_badge.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
public/static/img/veteran_8_badge.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
public/static/img/winner_badge.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -1,8 +1,8 @@
|
||||
import { Box, Text, Popover, Progress, Title } from "@mantine/core";
|
||||
import { Box, Text, Popover, Progress, Title, Image } from "@mantine/core";
|
||||
import { usePlayerBadges, useAllBadges } from "../queries";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { Badge, BadgeProgress } from "../types";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { MedalIcon, LockKeyIcon } from "@phosphor-icons/react";
|
||||
|
||||
interface BadgeShowcaseProps {
|
||||
@@ -16,6 +16,47 @@ interface BadgeDisplay {
|
||||
progressText: string;
|
||||
}
|
||||
|
||||
interface BadgeIconProps {
|
||||
badge: Badge;
|
||||
earned: boolean;
|
||||
}
|
||||
|
||||
const BadgeIcon = ({ badge, earned }: BadgeIconProps) => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const imagePath = `/static/img/${badge.key}.png`;
|
||||
|
||||
if (imageError) {
|
||||
// Fallback to icon if image fails to load
|
||||
return earned ? (
|
||||
<MedalIcon
|
||||
size={48}
|
||||
weight="fill"
|
||||
color="var(--mantine-primary-color-6)"
|
||||
/>
|
||||
) : (
|
||||
<LockKeyIcon
|
||||
size={44}
|
||||
weight="regular"
|
||||
color="var(--mantine-color-dimmed)"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={imagePath}
|
||||
alt={badge.name}
|
||||
width={48}
|
||||
height={48}
|
||||
onError={() => setImageError(true)}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
opacity: earned ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
const { user } = useAuth();
|
||||
const { data: badgeProgress } = usePlayerBadges(playerId);
|
||||
@@ -35,6 +76,15 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isVeteranBadge = /^veteran_\d+_badge$/.test(badge.key);
|
||||
if (isVeteranBadge && !earned) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (badge.key === 'new_player_badge') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let progressText = "";
|
||||
if (progress) {
|
||||
const target = getTargetProgress(badge);
|
||||
@@ -129,15 +179,14 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
<Box
|
||||
key={i}
|
||||
style={{
|
||||
aspectRatio: '1',
|
||||
width: '85px',
|
||||
height: '85px',
|
||||
borderRadius: '12px',
|
||||
background: 'transparent',
|
||||
border: '2px solid var(--mantine-primary-color-5)',
|
||||
position: 'absolute',
|
||||
top: `${(i + 1) * 3}px`,
|
||||
left: `${(i + 1) * 3}px`,
|
||||
right: `-${(i + 1) * 3}px`,
|
||||
bottom: `-${(i + 1) * 3}px`,
|
||||
opacity: 0.4 - (i * 0.15),
|
||||
zIndex: -(i + 1),
|
||||
}}
|
||||
@@ -148,9 +197,10 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
|
||||
<Box
|
||||
style={{
|
||||
aspectRatio: '1',
|
||||
width: '100px',
|
||||
height: '100px',
|
||||
borderRadius: '12px',
|
||||
background: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-7))',
|
||||
background: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
||||
border: display.earned
|
||||
? '2px solid var(--mantine-primary-color-6)'
|
||||
: '2px dashed var(--mantine-primary-color-4)',
|
||||
@@ -159,7 +209,6 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
padding: 'var(--mantine-spacing-xs)',
|
||||
position: 'relative',
|
||||
boxShadow: display.earned
|
||||
? '0 0 0 1px color-mix(in srgb, var(--mantine-primary-color-6) 20%, transparent)'
|
||||
@@ -168,19 +217,7 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{display.earned ? (
|
||||
<MedalIcon
|
||||
size={32}
|
||||
weight="fill"
|
||||
color="var(--mantine-primary-color-6)"
|
||||
/>
|
||||
) : (
|
||||
<LockKeyIcon
|
||||
size={28}
|
||||
weight="regular"
|
||||
color="var(--mantine-color-dimmed)"
|
||||
/>
|
||||
)}
|
||||
<BadgeIcon badge={display.badge} earned={display.earned} />
|
||||
|
||||
{showStack && (
|
||||
<Box
|
||||
@@ -202,15 +239,15 @@ const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Title
|
||||
order={6}
|
||||
<Text
|
||||
size="xs"
|
||||
fw={display.earned ? 600 : 500}
|
||||
ta="center"
|
||||
c={display.earned ? undefined : 'dimmed'}
|
||||
style={{ lineHeight: 1.1 }}
|
||||
>
|
||||
{display.badge.name}
|
||||
</Title>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Popover.Target>
|
||||
|
||||
@@ -170,6 +170,38 @@ export function createBadgesService(pb: PocketBase) {
|
||||
return tournamentsAttended;
|
||||
}
|
||||
|
||||
if (criteria.won_tournament !== undefined) {
|
||||
const tournamentId = criteria.won_tournament;
|
||||
|
||||
try {
|
||||
const tournamentMatches = await pb.collection("matches").getFullList({
|
||||
filter: `tournament = "${tournamentId}" && status = "ended"`,
|
||||
expand: 'home,away,home.players,away.players',
|
||||
});
|
||||
|
||||
const winnersMatches = tournamentMatches.filter(m => !m.is_losers_bracket);
|
||||
const finalsMatch = winnersMatches.reduce((highest: any, current: any) =>
|
||||
(!highest || current.lid > highest.lid) ? current : highest, null);
|
||||
|
||||
if (finalsMatch && finalsMatch.status === 'ended') {
|
||||
const finalsWinnerId = (finalsMatch.home_cups > finalsMatch.away_cups) ? finalsMatch.home : finalsMatch.away;
|
||||
|
||||
const winningTeam = finalsMatch.expand?.[finalsWinnerId === finalsMatch.home ? 'home' : 'away'];
|
||||
const winningPlayers = winningTeam?.expand?.players || winningTeam?.players || [];
|
||||
|
||||
const playerWon = winningPlayers.some((p: any) =>
|
||||
(typeof p === 'string' ? p : p.id) === playerId
|
||||
);
|
||||
|
||||
return playerWon ? 1 : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (criteria.tournament_wins !== undefined) {
|
||||
if (tournamentIds.size === 0) return 0;
|
||||
|
||||
@@ -342,26 +374,28 @@ export function createBadgesService(pb: PocketBase) {
|
||||
|
||||
const criteria = badge.criteria;
|
||||
|
||||
return (
|
||||
criteria.matches_played ||
|
||||
criteria.tournament_wins ||
|
||||
criteria.tournaments_attended ||
|
||||
criteria.overtime_matches ||
|
||||
criteria.overtime_wins ||
|
||||
criteria.consecutive_wins ||
|
||||
1
|
||||
);
|
||||
// Use explicit checks to handle 0 values correctly
|
||||
if (criteria.matches_played !== undefined) return criteria.matches_played;
|
||||
if (criteria.tournament_wins !== undefined) return criteria.tournament_wins;
|
||||
if (criteria.tournaments_attended !== undefined) return criteria.tournaments_attended;
|
||||
if (criteria.overtime_matches !== undefined) return criteria.overtime_matches;
|
||||
if (criteria.overtime_wins !== undefined) return criteria.overtime_wins;
|
||||
if (criteria.consecutive_wins !== undefined) return criteria.consecutive_wins;
|
||||
if (criteria.won_tournament !== undefined) return 1;
|
||||
if (criteria.placement !== undefined) return 1;
|
||||
if (criteria.margin_of_victory !== undefined) return 1;
|
||||
if (criteria.tournament_record !== undefined) return 1;
|
||||
|
||||
return 1;
|
||||
},
|
||||
|
||||
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,
|
||||
@@ -369,7 +403,6 @@ export function createBadgesService(pb: PocketBase) {
|
||||
return transformBadgeProgress(updated);
|
||||
}
|
||||
|
||||
// Create new progress record
|
||||
const created = await pb.collection("badge_progress").create({
|
||||
badge: badgeId,
|
||||
player: playerId,
|
||||
@@ -401,7 +434,11 @@ export function createBadgesService(pb: PocketBase) {
|
||||
try {
|
||||
const progress = await this.calculateBadgeProgress(playerId, badge);
|
||||
const target = this.getTargetProgress(badge);
|
||||
const earned = progress >= target;
|
||||
|
||||
const isPlacementBadge = badge.criteria.placement !== undefined;
|
||||
const earned = badge.progressive || isPlacementBadge
|
||||
? progress >= target
|
||||
: progress === target;
|
||||
|
||||
if (progress > 0 || earned) {
|
||||
await this.createBadgeProgress({
|
||||
|
||||