i18n
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / i18n Catalog Check (push) Failing after 10s
CI/CD Pipeline / Build and Push App Docker Image (push) Skipped
CI/CD Pipeline / Deploy to Kubernetes (push) Skipped

This commit is contained in:
2026-08-08 15:29:40 -07:00
parent 22c282d8fa
commit 0a7b5fa00f
178 changed files with 13099 additions and 1285 deletions
@@ -27,6 +27,7 @@ import InfiniteScroll from "@/components/infinite-scroll";
import { useNavigate } from "@tanstack/react-router";
import { useAllPlayerStats } from "../queries";
import { PlayerListItemSkeleton } from "./player-stats-table-skeleton";
import { Trans, useLingui } from "@lingui/react/macro";
type SortKey = keyof PlayerStats | "mmr";
type SortDirection = "asc" | "desc";
@@ -61,6 +62,7 @@ const StatCell = memo(({ label, value }: StatCellProps) => (
));
const PlayerListItem = memo(({ stat, onPlayerClick, mmr, onRegisterViewport, onUnregisterViewport }: PlayerListItemProps) => {
const { t } = useLingui();
const viewportRef = useRef<HTMLDivElement>(null);
const avg_cups_against = useMemo(() => stat.total_cups_against / stat.matches || 0, [stat.total_cups_against, stat.matches]);
@@ -103,11 +105,11 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr, onRegisterViewport, onU
</Text>
<Text size="xs" c="dimmed" ta="right">
{stat.matches}
<Text span fw={800}>M</Text>
<Text span fw={800}><Trans>M</Trans></Text>
</Text>
<Text size="xs" c="dimmed" ta="right">
{stat.tournaments}
<Text span fw={800}>T</Text>
<Text span fw={800}><Trans>T</Trans></Text>
</Text>
</Group>
@@ -125,16 +127,16 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr, onRegisterViewport, onU
}}
>
<Group gap='xs' wrap="nowrap">
<StatCell label="MMR" value={mmr.toFixed(1)} />
<StatCell label="W" value={stat.wins} />
<StatCell label="L" value={stat.losses} />
<StatCell label="W%" value={`${stat.win_percentage.toFixed(1)}%`} />
<StatCell label="AWM" value={stat.margin_of_victory?.toFixed(1) || 0} />
<StatCell label="ALM" value={stat.margin_of_loss?.toFixed(1) || 0} />
<StatCell label="AC" value={stat.avg_cups_per_match.toFixed(1)} />
<StatCell label="ACA" value={avg_cups_against?.toFixed(1) || 0} />
<StatCell label="CF" value={stat.total_cups_made} />
<StatCell label="CA" value={stat.total_cups_against} />
<StatCell label={t`MMR`} value={mmr.toFixed(1)} />
<StatCell label={t`W`} value={stat.wins} />
<StatCell label={t`L`} value={stat.losses} />
<StatCell label={t`W%`} value={`${stat.win_percentage.toFixed(1)}%`} />
<StatCell label={t`AWM`} value={stat.margin_of_victory?.toFixed(1) || 0} />
<StatCell label={t`ALM`} value={stat.margin_of_loss?.toFixed(1) || 0} />
<StatCell label={t`AC`} value={stat.avg_cups_per_match.toFixed(1)} />
<StatCell label={t`ACA`} value={avg_cups_against?.toFixed(1) || 0} />
<StatCell label={t`CF`} value={stat.total_cups_made} />
<StatCell label={t`CA`} value={stat.total_cups_against} />
</Group>
</ScrollArea>
</Stack>
@@ -170,6 +172,7 @@ const calculateMMR = (stat: PlayerStats): number => {
};
const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
const { t } = useLingui();
const { data: playerStats } = useAllPlayerStats(viewType);
const navigate = useNavigate();
const [search, setSearch] = useState("");
@@ -310,7 +313,7 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
<ChartBarIcon size={32} />
</ThemeIcon>
<Title order={3} c="dimmed">
No Stats Available
<Trans>No Stats Available</Trans>
</Title>
</Stack>
);
@@ -320,10 +323,10 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
<Container size="100%" px={0}>
<Stack gap="xs">
<Text px="md" size="10px" lh={0} c="dimmed">
Showing {filteredAndSortedStats.length} of {playerStats.length} players
<Trans>Showing {filteredAndSortedStats.length} of {playerStats.length} players</Trans>
</Text>
<TextInput
placeholder="Search players"
placeholder={t`Search players`}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
leftSection={<MagnifyingGlassIcon size={16} />}
@@ -334,13 +337,13 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
<Group px="md" justify="space-between" align="center">
<Group gap="xs" w="100%">
<div></div>
<Text ml='auto' size="xs" c="dimmed">Sort:</Text>
<Text ml='auto' size="xs" c="dimmed"><Trans>Sort:</Trans></Text>
<UnstyledButton
onClick={() => handleSort("mmr")}
style={{ display: "flex", alignItems: "center", gap: 4 }}
>
<Text size="xs" fw={sortConfig.key === "mmr" ? 600 : 400} c={sortConfig.key === "mmr" ? "var(--mantine-color-text)" : "dimmed"}>
MMR
<Trans>MMR</Trans>
</Text>
{getSortIcon("mmr")}
</UnstyledButton>
@@ -350,7 +353,7 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
style={{ display: "flex", alignItems: "center", gap: 4 }}
>
<Text size="xs" fw={sortConfig.key === "wins" ? 600 : 400} c={sortConfig.key === "wins" ? "var(--mantine-color-text)" : "dimmed"}>
Wins
<Trans>Wins</Trans>
</Text>
{getSortIcon("wins")}
</UnstyledButton>
@@ -360,80 +363,80 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
style={{ display: "flex", alignItems: "center", gap: 4 }}
>
<Text size="xs" fw={sortConfig.key === "matches" ? 600 : 400} c={sortConfig.key === "matches" ? "var(--mantine-color-text)" : "dimmed"}>
Matches
<Trans>Matches</Trans>
</Text>
{getSortIcon("matches")}
</UnstyledButton>
<Popover position="bottom-end" withArrow shadow="md">
<Popover.Target>
<ActionIcon variant="subtle" size="sm" aria-label="Stat abbreviations and MMR info">
<ActionIcon variant="subtle" size="sm" aria-label={t`Stat abbreviations and MMR info`}>
<InfoIcon size={14} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
<Box maw={280}>
<Text size="sm" fw={500} mb="xs">
Stat Abbreviations:
<Trans>Stat Abbreviations:</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>M:</strong> Matches
<Trans>• <strong>M:</strong> Matches</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>T:</strong> Tournaments
<Trans>• <strong>T:</strong> Tournaments</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>MMR:</strong> Matchmaking Rating
<Trans>• <strong>MMR:</strong> Matchmaking Rating</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>W:</strong> Wins
<Trans>• <strong>W:</strong> Wins</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>L:</strong> Losses
<Trans>• <strong>L:</strong> Losses</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>W%:</strong> Win Percentage
<Trans>• <strong>W%:</strong> Win Percentage</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>AWM:</strong> Average Win Margin
<Trans>• <strong>AWM:</strong> Average Win Margin</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>ALM:</strong> Average Loss Margin
<Trans>• <strong>ALM:</strong> Average Loss Margin</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>AC:</strong> Average Cups Per Match
<Trans>• <strong>AC:</strong> Average Cups Per Match</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>ACA:</strong> Average Cups Against
<Trans>• <strong>ACA:</strong> Average Cups Against</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>CF:</strong> Cups For
<Trans>• <strong>CF:</strong> Cups For</Trans>
</Text>
<Text size="xs" mb={2}>
• <strong>CA:</strong> Cups Against
<Trans>• <strong>CA:</strong> Cups Against</Trans>
</Text>
<Divider my="sm" />
<Text size="sm" fw={500} mb="xs">
MMR Calculation:
<Trans>MMR Calculation:</Trans>
</Text>
<Text size="xs" mb={2}>
• Win Rate (50%)
<Trans>• Win Rate (50%)</Trans>
</Text>
<Text size="xs" mb={2}>
• Average Cups/Match (25%)
<Trans>• Average Cups/Match (25%)</Trans>
</Text>
<Text size="xs" mb={2}>
• Average Win Margin (15%)
<Trans>• Average Win Margin (15%)</Trans>
</Text>
<Text size="xs" mb={2}>
• Match Volume Bonus (10%)
<Trans>• Match Volume Bonus (10%)</Trans>
</Text>
<Text size="xs" mt="xs" c="dimmed">
* Confidence penalty applied for players with &lt;15 matches
<Trans>* Confidence penalty applied for players with &lt;15 matches</Trans>
</Text>
<Text size="xs" mt="xs" c="dimmed">
** Not an official rating
<Trans>** Not an official rating</Trans>
</Text>
</Box>
</Popover.Dropdown>
@@ -470,7 +473,7 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
{filteredAndSortedStats.length === 0 && search && (
<Text ta="center" c="dimmed" py="xl">
No players found matching "{search}"
<Trans>No players found matching "{search}"</Trans>
</Text>
)}
</Stack>