This commit is contained in:
tresabhi committed 2026-08-06 20:01:50 -05:00
1 parent 13cda7aec0
commit b26c5137f3
53 files changed
+236 -932

No files matched your search

@@ -1,4 +1,4 @@
import { isExplosive } from "@blitzkit/core";
import { alias, isExplosive } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Flex, Heading, IconButton, Tooltip } from "@radix-ui/themes";
import { useLocale } from "../../../../../../hooks/useLocale";
@@ -62,7 +62,7 @@ export function Firepower({
alt={unwrap(thisShell.name!)}
width={16}
height={16}
src={`/api/icons/shells/${thisShell.icon}.webp`}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
/>
</IconButton>
</Tooltip>
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Flex, IconButton, Tooltip } from "@radix-ui/themes";
import { useLocale } from "../../../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../../../stores/duel";
@@ -37,7 +38,7 @@ export function ShellSwitcher() {
<img
alt={unwrap(thisShell.name!)}
style={{ width: "1.125rem", height: "1.125rem" }}
src={`/api/icons/shells/${thisShell.icon}.webp`}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
/>
</IconButton>
</Tooltip>
@@ -1,4 +1,4 @@
import { isExplosive } from "@blitzkit/core";
import { alias, isExplosive } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Box, Flex, Text } from "@radix-ui/themes";
import { useRef, useState } from "react";
@@ -124,7 +124,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
/>
<img
src={`/api/icons/shells/${shell.icon}.webp`}
src={alias("api", `/icons/shells/${shell.icon}.webp`)}
style={{
objectFit: "contain",
transform: "translate(-50%, -50%) rotate(-45deg)",
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Badge, Box, Flex, Heading, Switch, Text } from "@radix-ui/themes";
import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from "../../../../stores/duel";
@@ -33,7 +34,7 @@ export function Equalizer() {
style={{
cursor: "pointer",
borderRadius: "var(--radius-3)",
backgroundImage: "url(/api/gamemodes/45/banner.webp)",
backgroundImage: `url(${alias("api", `/gamemodes/45/banner.webp`)})`,
backgroundSize: "cover",
backgroundPosition: "center",
overflow: "hidden",
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Flex, Heading } from "@radix-ui/themes";
import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from "../../../../stores/duel";
@@ -21,7 +22,7 @@ export function Miscellaneous() {
<Flex gap="2" align="center">
{!tank.fixed_camouflage && (
<GenericTankComponentButton
icon={"/api/icons/camo.webp"}
icon={alias("api", "/icons/camo.webp")}
selected={camouflage}
iconStyles={{
top: "50%",
@@ -36,7 +37,7 @@ export function Miscellaneous() {
/>
)}
<GenericTankComponentButton
icon={"/api/icons/boosters/equipment.webp"}
icon={alias("api", "/icons/boosters/equipment.webp")}
selected={cooldownBooster > 0}
banner={
cooldownBooster === 0
@@ -1,4 +1,5 @@
import {
alias,
EngineDefinition,
formatCompact,
GunDefinition,
@@ -110,7 +111,7 @@ function ModuleButton({
<img
alt={unlock.cost!.type}
src={`/api/icons/currencies/${unlock.cost!.type}.webp`}
src={alias("api", `/icons/currencies/${unlock.cost!.type}.webp`)}
style={{
width: 12,
height: 12,
@@ -125,7 +126,10 @@ function ModuleButton({
src={
isTank
? tankIcon(unlock.id)
: `/api/icons/modules/${moduleTypeTypeIconName[unlock.type]}.webp`
: alias(
"api",
`/icons/modules/${moduleTypeTypeIconName[unlock.type]}.webp`,
)
}
style={{
width: isTank ? 64 : 32,
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Box, Flex, Heading, Text } from "@radix-ui/themes";
import { api } from "../../core/blitzkit/api";
import { useLocale } from "../../hooks/useLocale";
@@ -37,7 +38,10 @@ export function GameModeSection() {
direction="column"
style={{
borderRadius: "var(--radius-3)",
background: `url(/api/gamemodes/${id}/banner.webp)`,
background: `url(${alias(
"api",
`/gamemodes/${id}/banner.webp`,
)})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
@@ -45,7 +49,10 @@ export function GameModeSection() {
<Box
flexGrow="1"
style={{
background: `url(/api/gamemodes/${id}/banner.webp)`,
background: `url(${alias(
"api",
`/gamemodes/${id}/banner.webp`,
)})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
@@ -80,7 +87,7 @@ export function GameModeSection() {
height: "1.5em",
objectFit: "contain",
}}
src={`/api/icons/provisions/${id}.webp`}
src={alias("api", `/icons/provisions/${id}.webp`)}
alt={unwrap(provisions.name!)}
/>
{unwrap(provisions.name!)}
@@ -101,7 +108,7 @@ export function GameModeSection() {
height: "1.5em",
objectFit: "contain",
}}
src={`/api/icons/consumables/${id}.webp`}
src={alias("api", `/icons/consumables/${id}.webp`)}
alt={unwrap(consumable.name!)}
/>
{unwrap(consumable.name!)}
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { IconButton } from "@radix-ui/themes";
import { useArmor } from "../../../../../../hooks/useArmor";
import { Duel } from "../../../../../../stores/duel";
@@ -37,7 +38,7 @@ export function DynamicArmorSwitcher() {
>
<img
alt="Dynamic Armor"
src={"/api/icons/consumables/73.webp"}
src={alias("api", "/icons/consumables/73.webp")}
style={{
width: "50%",
height: "50%",
@@ -1,4 +1,5 @@
import {
alias,
resolvePenetrationCoefficient,
TIER_ROMAN_NUMERALS,
uniqueGuns,
@@ -139,7 +140,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<img
alt="Antagonist Gun"
src={"/api/icons/modules/gun.webp"}
src={alias("api", "/icons/modules/gun.webp")}
style={{
width: "65%",
height: "65%",
@@ -208,7 +209,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
>
<img
alt={unwrap(thisShell.name!)}
src={`/api/icons/shells/${thisShell.icon}.webp`}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
style={{
width: "50%",
height: "50%",
@@ -245,7 +246,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
>
<img
alt="Calibrated Shells"
src={"/api/icons/equipment/103.webp"}
src={alias("api", "/icons/equipment/103.webp")}
style={{
width: "50%",
height: "50%",
@@ -270,7 +271,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
>
<img
alt="Enhanced Armor"
src={"/api/icons/equipment/110.webp"}
src={alias("api", "/icons/equipment/110.webp")}
style={{
width: "50%",
height: "50%",
@@ -1,3 +1,4 @@
import { alias } from "@blitzkit/core";
import { Flex, type FlexProps, Progress } from "@radix-ui/themes";
import { useProgress } from "@react-three/drei";
@@ -11,7 +12,7 @@ export function TankSandboxLoader({ id, ...props }: TankSandboxLoaderProps) {
return (
<Flex width="100%" height="100%" align="center" justify="center" {...props}>
<img
src={`/api/tanks/${id}/icons/big.webp`}
src={alias("api", `/tanks/${id}/icons/big.webp`)}
style={{ height: "75%", filter: "blur(1rem)" }}
/>
<Progress
@@ -1,5 +1,5 @@
import { Flex, Text, type TextProps } from '@radix-ui/themes';
import { Var } from '../../../../core/radix/var';
import { Flex, Text, type TextProps } from "@radix-ui/themes";
import { Var } from "../../../../core/radix/var";
type ListingProps = TextProps & {
label: string;
@@ -11,7 +11,7 @@ export function Listing({ label, ...props }: ListingProps) {
minWidth="17rem"
justify="between"
gap="4"
style={{ borderBottom: `1px solid ${Var('gray-a9')}` }}
style={{ borderBottom: `1px solid ${Var("gray-a9")}` }}
>
<Text size="4">{label}</Text>
<Text align="right" size="4" weight="medium" {...props} />
@@ -1,25 +0,0 @@
import { StarFilledIcon, StarIcon } from '@radix-ui/react-icons';
import { Box, type BoxProps } from '@radix-ui/themes';
type PartialStarProps = BoxProps & {
fill: number;
};
export function PartialStar({ fill, ...props }: PartialStarProps) {
return (
<Box height="1em" width="1em" position="relative" {...props}>
<StarIcon width="1em" height="1em" />
{fill > 0 && (
<Box
top="0"
left="0"
width={`${fill * 100}%`}
position="absolute"
overflow="hidden"
>
<StarFilledIcon width="1em" height="1em" />
</Box>
)}
</Box>
);
}
@@ -1,44 +0,0 @@
import type { StarsInt } from '@blitzkit/core';
import { type TextProps, Flex, Text } from '@radix-ui/themes';
import { times } from 'lodash-es';
import { PartialStar } from './components/PartialStar';
type StarsProps = TextProps & {
stars: number | null;
onCast?: (value: StarsInt) => void;
};
export function Stars({ stars, onCast, ...props }: StarsProps) {
let color: TextProps['color'] = 'green';
if (stars === null) color = 'gray';
else if (stars < 1.5) color = 'tomato';
else if (stars < 2.5) color = 'orange';
else if (stars < 3.5) color = 'amber';
else if (stars < 4.5) color = 'blue';
return (
<Text color={color} {...props}>
<Flex style={{ cursor: onCast ? 'pointer' : 'unset' }}>
{stars !== null &&
times(Math.floor(stars), (index) => (
<PartialStar
fill={1}
key={index}
onClick={() => onCast?.((index + 1) as StarsInt)}
/>
))}
{stars !== null && stars % 1 !== 0 && <PartialStar fill={stars % 1} />}
{times(5 - Math.ceil(stars ?? 0), (index) => (
<PartialStar
fill={0}
key={index}
onClick={() =>
onCast?.((Math.floor(stars ?? 0) + index + 1) as StarsInt)
}
/>
))}
</Flex>
</Text>
);
}
@@ -1,18 +0,0 @@
import { Flex, Text } from '@radix-ui/themes';
import { Stars } from '../../Stars';
interface StarRowProps {
stars: number | null;
children: string;
}
export function StarRow({ stars, children }: StarRowProps) {
return (
<Text color={stars === null ? 'gray' : undefined}>
<Flex align="center" justify="between" width="15rem">
{children}
<Stars stars={stars} />
</Flex>
</Text>
);
}
@@ -1,179 +0,0 @@
import type { BlitzkitResponse, StarsInt } from "@blitzkit/core";
import { PaperPlaneIcon } from "@radix-ui/react-icons";
import {
AlertDialog,
Button,
Dialog,
Flex,
Spinner,
Text,
} from "@radix-ui/themes";
import { useState } from "react";
import { useLocale } from "../../../../../../hooks/useLocale";
import { App } from "../../../../../../stores/app";
import { Duel } from "../../../../../../stores/duel";
import { Stars } from "../../Stars";
export function VoteCaster() {
const tank = Duel.use((state) => state.protagonist.tank);
const [easiness, setEasiness] = useState<StarsInt | null>(null);
const [firepower, setFirepower] = useState<StarsInt | null>(null);
const [maneuverability, setManeuverability] = useState<StarsInt | null>(null);
const [survivability, setSurvivability] = useState<StarsInt | null>(null);
// true = vote successful, false = vote failed, null = hide alert
const [postVote, setPostVote] = useState<boolean | null>(null);
// true = open, false = closed, null = casting
const [showDialogue, setShowDialogue] = useState<boolean | null>(false);
const { unwrap } = useLocale();
return (
<>
<Dialog.Root open={showDialogue !== false} onOpenChange={setShowDialogue}>
<Dialog.Trigger>
<Button variant="surface">
<PaperPlaneIcon /> Vote
</Button>
</Dialog.Trigger>
<Dialog.Content maxWidth="23rem">
<Dialog.Title>Voting for {unwrap(tank.name!)}</Dialog.Title>
<Flex direction="column" mt="4">
<Flex align="center" justify="between">
<Text>
Easiness{" "}
<Text color="gray" size="2">
{easiness === null ? "(empty)" : `(${easiness})`}
</Text>
</Text>
<Stars stars={easiness} size="5" onCast={setEasiness} />
</Flex>
<Flex align="center" justify="between">
<Text>
Firepower{" "}
<Text color="gray" size="2">
{firepower === null ? "(empty)" : `(${firepower})`}
</Text>
</Text>
<Stars stars={firepower} size="5" onCast={setFirepower} />
</Flex>
<Flex align="center" justify="between">
<Text>
Maneuverability{" "}
<Text color="gray" size="2">
{maneuverability === null
? "(empty)"
: `(${maneuverability})`}
</Text>
</Text>
<Stars
stars={maneuverability}
size="5"
onCast={setManeuverability}
/>
</Flex>
<Flex align="center" justify="between">
<Text>
Survivability{" "}
<Text color="gray" size="2">
{survivability === null ? "(empty)" : `(${survivability})`}
</Text>
</Text>
<Stars stars={survivability} size="5" onCast={setSurvivability} />
</Flex>
</Flex>
<Flex justify="end" mt="4" gap="2">
<Dialog.Close>
<Button variant="outline" color="red">
Cancel
</Button>
</Dialog.Close>
<Button
disabled={
showDialogue === null ||
(easiness === null &&
firepower === null &&
maneuverability === null &&
survivability === null)
}
onClick={() => {
const { wargaming } = App.state.logins;
if (!wargaming) {
return setShowDialogue(false);
}
setShowDialogue(null);
fetch(
`/api/tank-voting/${tank.id}/cast?${
easiness === null ? "" : `easiness=${easiness}&`
}${firepower === null ? "" : `firepower=${firepower}&`}${
maneuverability === null
? ""
: `maneuverability=${maneuverability}&`
}${
survivability === null
? ""
: `survivability=${survivability}&`
}player=${wargaming.id}&token=${wargaming.token}`,
)
.then(
(response) => response.json() as Promise<BlitzkitResponse>,
)
.then((json) => {
setShowDialogue(false);
setPostVote(json.status === "ok");
if (json.status === "error") {
console.error("vote failed", json);
}
});
}}
>
{showDialogue === null ? <Spinner /> : <PaperPlaneIcon />} Vote
</Button>
</Flex>
</Dialog.Content>
</Dialog.Root>
<AlertDialog.Root
open={postVote !== null}
onOpenChange={() => setPostVote(null)}
>
{postVote === true && (
<AlertDialog.Content maxWidth="20rem">
<AlertDialog.Title>Vote casted</AlertDialog.Title>
<AlertDialog.Description>
It may take a few minutes for it to show up.
</AlertDialog.Description>
<Flex justify="end" mt="4">
<AlertDialog.Cancel>
<Button>Okay</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
)}
{postVote === false && (
<AlertDialog.Content maxWidth="20rem">
<AlertDialog.Title>Voting failed</AlertDialog.Title>
<AlertDialog.Description>
So sorry about this! If you're a nerd, feel free to check out the
console.
</AlertDialog.Description>
<Flex justify="end" mt="4">
<AlertDialog.Cancel>
<Button>Gotcha</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
)}
</AlertDialog.Root>
</>
);
}
@@ -1,91 +0,0 @@
import { formatCompact } from '@blitzkit/core';
import { Flex, Heading, Skeleton, Text } from '@radix-ui/themes';
import { memo } from 'react';
import { useLocale } from '../../../../../hooks/useLocale';
import { useTankVotes } from '../../../../../hooks/useTankVotes';
import { App } from '../../../../../stores/app';
import { Duel } from '../../../../../stores/duel';
import { WargamingLoginButton } from '../../../../WargamingLoginButton';
import { StarRow } from './components/StarRow';
import { VoteCaster } from './components/VoteCaster';
export const Votes = memo(
() => {
const id = Duel.use((state) => state.protagonist.tank.id);
const votes = useTankVotes(id);
const wargaming = App.use((state) => state.logins.wargaming);
const { locale } = useLocale();
return (
<Flex direction="column" gap="3">
<Flex justify="between" align="center">
<Heading size="4">
{!votes && <Skeleton height="1em" width="4rem" />}
{votes && formatCompact(locale, votes.votes)}{' '}
{votes && (votes.votes > 1 ? 'votes' : 'vote')}
</Heading>
{!wargaming && (
<WargamingLoginButton variant="surface">
Login to vote
</WargamingLoginButton>
)}
{wargaming && <VoteCaster />}
</Flex>
<Flex gap="0" direction="column">
<Flex
gap={{ initial: '0', sm: '6' }}
direction={{ initial: 'column', sm: 'row' }}
>
<StarRow
stars={votes?.categories.easiness ?? null}
children="Easiness"
/>
<StarRow
stars={votes?.categories.firepower ?? null}
children="Firepower"
/>
</Flex>
<Flex
gap={{ initial: '0', sm: '6' }}
direction={{ initial: 'column', sm: 'row' }}
>
<StarRow
stars={votes?.categories.maneuverability ?? null}
children="Maneuverability"
/>
<StarRow
stars={votes?.categories.survivability ?? null}
children="Survivability"
/>
</Flex>
<Flex justify="center">
<Text size="2" color="gray" mt="2">
<>
{votes ? (
wargaming ? (
votes.last_updated ? (
`You voted on ${new Date(
votes.last_updated,
).toLocaleDateString()}`
) : (
`You haven't voted yet`
)
) : (
'Login to vote'
)
) : (
<Skeleton height="1em" width="5rem" />
)}
</>
</Text>
</Flex>
</Flex>
</Flex>
);
},
() => true,
);
@@ -1,4 +1,9 @@
import { TankPriceType, TankType, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import {
alias,
TankPriceType,
TankType,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import { Box, Flex } from "@radix-ui/themes";
import { Var } from "../../../core/radix/var";
import { useLocale } from "../../../hooks/useLocale";
@@ -35,7 +40,7 @@ export function MetaSection() {
>
<Box
style={{
background: `url(${`/api/flags/scratched/${protagonist.nation}.webp`})`,
background: `url(${alias("api", `/flags/scratched/${protagonist.nation}.webp`)})`,
backgroundRepeat: "no-repeat",
backgroundSize: "min(53rem, 80vw)",
backgroundPosition: "-8rem 50%",
@@ -60,7 +65,10 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt={protagonist.nation}
src={`/api/flags/circle/${protagonist.nation}.webp`}
src={alias(
"api",
`/flags/circle/${protagonist.nation}.webp`,
)}
/>
{
strings.common.nations[
@@ -130,7 +138,7 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt="gold"
src={"/api/icons/currencies/gold.webp"}
src={alias("api", "/icons/currencies/gold.webp")}
/>
</Flex>
</Listing>
@@ -147,12 +155,15 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt={TankPriceType[protagonist.price!.type]}
src={`/api/icons/currencies/${
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`}
src={alias(
"api",
`/icons/currencies/${
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`,
)}
/>
</Flex>
</Listing>
@@ -162,12 +173,15 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt={TankPriceType[protagonist.price!.type]}
src={`/api/icons/currencies/${
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`}
src={alias(
"api",
`/icons/currencies/${
protagonist.price!.type ===
TankPriceType.TANK_PRICE_TYPE_GOLD
? "gold"
: "silver"
}.webp`,
)}
/>
</Flex>
</Listing>
@@ -183,7 +197,7 @@ export function MetaSection() {
<img
style={{ width: "1em", height: "1em" }}
alt="xp"
src={"/api/icons/currencies/xp.webp"}
src={alias("api", "/icons/currencies/xp.webp")}
/>
</Flex>
</Listing>
@@ -1,4 +1,4 @@
import { formatCompact, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { alias, formatCompact, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import { Box, Flex, Skeleton, Text } from "@radix-ui/themes";
import { awaitableAverageDefinitions } from "../../../../core/awaitables/averageDefinitions";
@@ -58,7 +58,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
mx={highlight ? "2" : "0"}
style={{
background: highlight
? `url(${`/api/flags/fade/${tank.nation}.webp`})`
? `url(${alias("api", `/flags/fade/${tank.nation}.webp`)})`
: undefined,
backgroundPosition: "center",
backgroundSize: "cover",
@@ -79,7 +79,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
>
<img
alt={unwrap(tank.name!)}
src={`/api/tanks/${id}/icons/big.webp`}
src={alias("api", `/tanks/${id}/icons/big.webp`)}
width={64}
height={64}
style={{
@@ -99,7 +99,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
<Flex gap="1" align="center">
<img
alt="XP"
src={"/api/icons/currencies/xp.webp"}
src={alias("api", "/icons/currencies/xp.webp")}
style={{
width: "1em",
height: "1em",
@@ -114,7 +114,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
<Flex gap="1" align="center">
<img
alt="Silver"
src={"/api/icons/currencies/silver.webp"}
src={alias("api", "/icons/currencies/silver.webp")}
style={{
width: "1em",
height: "1em",
@@ -1,4 +1,4 @@
import { TankType } from "@blitzkit/core";
import { alias, TankType } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from "@radix-ui/react-icons";
import { Flex, Heading, IconButton, ScrollArea, Text } from "@radix-ui/themes";
@@ -105,7 +105,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
<Flex gap="1" align="center">
<img
alt="XP"
src={"/api/icons/currencies/xp.webp"}
src={alias("api", "/icons/currencies/xp.webp")}
style={{
width: "1em",
height: "1em",
@@ -121,7 +121,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
<Flex gap="1" align="center">
<img
alt="Silver"
src={"/api/icons/currencies/silver.webp"}
src={alias("api", "/icons/currencies/silver.webp")}
style={{
width: "1em",
height: "1em",