Merge branch 'main' into ue

This commit is contained in:
tresabhi committed 2025-04-05 17:18:36 -05:00
commit 2a8ca6d36f
73 files changed
+1068 -512

No files matched your search

+1
View File
@@ -37,6 +37,7 @@ env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
jobs:
build:
+3
View File
@@ -15,6 +15,7 @@
"arrayfy",
"artefacts",
"astrojs",
"astrolib",
"atgm",
"Autoloader",
"Autoreloader",
@@ -138,9 +139,11 @@
"outfile",
"ownedtanks",
"paks",
"partytown",
"PIXELFORMAT",
"playerachievements",
"playerinfo",
"playform",
"pngjs",
"POSITIVEX",
"POSITIVEY",
+3
View File
@@ -0,0 +1,3 @@
# FreakKit 1.27.4
It has come to our attention that tanks are very harsh objects. To even it out, we made the website a pretty pretty princess <3
+11
View File
@@ -0,0 +1,11 @@
# BlitzKit 1.27.5
This update only affects the bot.
## Changes
- Removed `/auction`
## Fixes
- Fixed a bug that caused `/rating` to disappear
+23
View File
@@ -0,0 +1,23 @@
# BlitzKit 1.27.6
This update only affects the website.
## Features
- Added a promotional card for [the official Discord server](https://discord.gg/nDt7AjGJQH) to the homepage
- Added a better navbar
- The added spaces and padding helps add more room for error when clicking, especially on mobile
- The dropdown menu is now a lost smaller, disposing of the need to scroll at all
- Added shell filters to the tank search
- Reworked the filter buttons and their positions to allow for more space
## Fixes
- Moved most images from imgur to BlitzKit allowing for users in certain countries to load BlitzKit without getting blocked
## Technical Features
- Added Partytown to offload some workload off the main thread, allowing for a more responsive experience
- Reworked `robots.txt` to disallow indexing of BlitzKit variants
- Improved the SEO further for most pages
- Added a few compressor library to speed up load times
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "blitzkit",
"private": true,
"version": "1.27.3",
"version": "1.27.6",
"description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module",
"types": "./types.d.ts",
@@ -16,8 +16,8 @@ export function CommandWrapper({ children, fat }: CommandWrapperProps) {
gap: 16,
width: '100%',
color: theme.colors.textHighContrast,
backgroundColor: theme.colors.appBackground1,
position: 'relative',
background: theme.colors.appBackground1,
}}
>
{children}
@@ -15,7 +15,8 @@ export function localizationObject(
(localizations, locale) => {
const { strings } = translator(locale);
const translation = literalsFunc(path(strings), literals);
const isValid = !strict || validNameRegex.test(translation);
const isValid =
(!strict || validNameRegex.test(translation)) && translation.length > 0;
if (!isValid) {
console.warn(
@@ -19,11 +19,11 @@ import { Writeable } from '../../../types/writable';
export async function handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
await interaction.deferReply();
const awaitedCommands = await commands;
const registry = awaitedCommands[interaction.commandName];
await interaction.deferReply();
try {
const returnable = await registry.handler(interaction);
@@ -15,7 +15,6 @@ import {
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
import { aboutCommand } from '../../commands/about';
import { auctionCommand } from '../../commands/auction';
import { breakdownCommand } from '../../commands/breakdown';
import { debugCommand } from '../../commands/debug';
import { evolutionCommand } from '../../commands/evolution';
@@ -80,7 +79,7 @@ interface CommandRegistryExplicit extends CommandRegistryBase {
export type CommandRegistry = CommandRegistryImplicit | CommandRegistryExplicit;
export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
auctionCommand,
// auctionCommand,
permissionsCommand,
debugCommand,
aboutCommand,
+8 -1
View File
@@ -45,7 +45,7 @@ const interval = setInterval(async () => {
const usernames = awaitedDiscoveredIdsDefinitions.count;
manager.broadcastEval(
(client, { servers, users, channels, tanks, usernames }) => {
async (client, { servers, users, channels, tanks, usernames }) => {
const bios = [
`Living in ${servers.toLocaleString()} servers`,
`Serving ${users.toLocaleString()} users`,
@@ -62,6 +62,13 @@ const interval = setInterval(async () => {
},
],
});
for (const guild of client.guilds.cache.values()) {
try {
const botMember = await guild.members.fetch(client.user.id);
await botMember.setNickname('BlitzKit');
} catch {}
}
},
{ context: { servers, channels, users, tanks, usernames } },
);
+1 -1
View File
@@ -23,7 +23,7 @@ export const theme = {
spaces: createSpaces(),
colors: {
...createColors(slateDark),
...createColors(mauveDark),
...createColors(slateDarkA, 'alpha'),
...createColors(slate, 'light'),
+14
View File
@@ -756,6 +756,7 @@
},
"tank_search": {
"clear_filters": "Clear",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
"select_all": "Select all",
@@ -824,6 +825,12 @@
},
"home": {
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
"action": "Discord"
},
"hero": {
"credit": "By %s1 ❤️"
},
@@ -838,6 +845,13 @@
"seo_description": "Tools for everything World of Tanks Blitz"
},
"docs": {
"patches": {
"name": "BlitzKit Changelogs",
"seo_description": "Changelogs for BlitzKit Discord bot and website"
}
},
"tools": {
"api": {
"name": "API",
@@ -1,4 +1,3 @@
import { imgur } from '@blitzkit/core';
import {
CodeIcon,
HeightIcon,
@@ -63,7 +62,7 @@ function Content({ embed }: PageProps) {
const embedStateStore = EmbedState.useStore();
const config = embedConfigurations[embed] as EmbedConfig;
const [backgroundImage, setBackgroundImage] = useState(
imgur('SO13zur', { format: 'jpeg' }),
'/assets/images/backgrounds/embed-default.webp',
);
const fileInput = useRef<HTMLInputElement>();
const { strings } = useLocale();
@@ -1,5 +1,4 @@
---
import { capitalize } from 'lodash-es';
import EmbedLayout from '../../../../../../layouts/EmbedLayout.astro';
import type { getStaticPaths } from '../index.astro';
import { Page } from './_index';
@@ -7,8 +6,6 @@ import { Page } from './_index';
export { getStaticPaths } from '../index.astro';
---
<title>BlitzKit - {capitalize(Astro.params.embed)} embed</title>
<EmbedLayout>
<Page client:only="react" embed={Astro.params.embed} />
</EmbedLayout>
@@ -1,4 +1,5 @@
---
import { AstroSeo } from '@astrolib/seo';
import type { GetStaticPaths } from 'astro';
import { capitalize } from 'lodash-es';
import { embedConfigurations } from '../../../../../constants/embeds';
@@ -21,10 +22,24 @@ export const getStaticPaths = (() => {
.flat();
}) satisfies GetStaticPaths;
const name = capitalize(Astro.params.embed);
const title = `BlitzKit - ${name} embed`;
const description = `BlitzKit ${name} embeds for streaming World of Tanks Blitz with extreme customization`;
const { locale } = Astro.params;
---
<title>BlitzKit - {capitalize(Astro.params.embed)} embed</title>
<AstroSeo
title={title}
description={description}
canonical={Astro.url.href}
openGraph={{
url: Astro.url.href,
title,
description,
site_name: 'BlitzKit',
}}
twitter={{ cardType: 'summary_large_image' }}
/>
<style is:global>
@keyframes fade-out-border {
+2 -2
View File
@@ -1,4 +1,3 @@
import { imgur, ImgurSize } from '@blitzkit/core';
import { Box, Flex, Link, Text } from '@radix-ui/themes';
import { PreviewWrapper } from '../../../../components/Embeds/PreviewWrapper';
import { LinkI18n } from '../../../../components/LinkI18n';
@@ -85,7 +84,8 @@ function Content() {
overflow="hidden"
position="relative"
style={{
backgroundImage: `url(${imgur('SO13zur', { format: 'jpeg', size: ImgurSize.SmallThumbnail })})`,
backgroundImage:
'url(/assets/images/backgrounds/embed-default.webp)',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
+13 -1
View File
@@ -1,8 +1,12 @@
// @ts-check
import partytown from '@astrojs/partytown';
import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
import playformCompress from '@playform/compress';
import playformInline from '@playform/inline';
import compressor from 'astro-compressor';
import { defineConfig } from 'astro/config';
export default defineConfig({
@@ -24,7 +28,15 @@ export default defineConfig({
concurrency: 8,
},
integrations: [react(), sitemap()],
integrations: [
react(),
sitemap(),
partytown(),
playformCompress(),
compressor(),
playformInline(),
],
vite: {
resolve: {
alias: {
+6 -1
View File
@@ -11,6 +11,8 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/partytown": "^2.1.4",
"@astrolib/seo": "^1.0.0-beta.8",
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@nanostores/persistent": "^0.10.2",
@@ -42,10 +44,13 @@
"@astrojs/check": "^0.9.4",
"@astrojs/react": "^3.6.2",
"@astrojs/sitemap": "^3.2.1",
"@playform/compress": "^0.1.9",
"@playform/inline": "^0.1.2",
"@types/js-cookie": "^3.0.6",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/three": "^0.160.0",
"astro": "^4.16.7"
"astro": "^4.16.7",
"astro-compressor": "^1.0.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

@@ -1,4 +1,3 @@
import { imgur } from '@blitzkit/core';
import { Flex, Heading, Link, Text } from '@radix-ui/themes';
import { Var } from '../core/radix/var';
@@ -18,7 +17,7 @@ export function AesonPlug() {
}}
>
<img
src={imgur('akrxfFC')}
src="/assets/images/third-party/wotb-news.png"
style={{ width: '6rem', height: '6rem', objectFit: 'cover' }}
/>
@@ -13,7 +13,7 @@ export function BlitzKitTheme({
}: BlitzKitThemeProps) {
return (
<Theme
style={{ minHeight: 'unset', ...style }}
style={{ minHeight: 'unset', display: 'contents', ...style }}
className={`blitzkit-theme ${className}`}
grayColor="mauve"
appearance="dark"
@@ -0,0 +1,21 @@
:root {
--scroll-time: 80s;
}
.scroller-0 {
animation: move-1 var(--scroll-time) linear infinite;
}
.scroller-1 {
animation-delay: var(--scroll-time);
animation: move-1 var(--scroll-time) linear infinite;
}
@keyframes move-1 {
from {
transform: translateY(0);
}
to {
transform: translateY(-100%);
}
}
@@ -0,0 +1,230 @@
import { assertSecret } from '@blitzkit/core';
import { CaretRightIcon } from '@radix-ui/react-icons';
import { Box, Button, Flex, Link, Text } from '@radix-ui/themes';
import { times } from 'lodash-es';
import { getStrings } from '../../core/i18n/getStrings';
import { Var } from '../../core/radix/var';
import type { LocaleAcceptorProps } from '../../hooks/useLocale';
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
import { InlineSkeleton } from '../InlineSkeleton';
import './index.css';
export interface DiscordWidget {
id: string;
name: string;
instant_invite: string;
channels: any[];
members: Member[];
presence_count: number;
}
export interface Member {
id: string;
username: string;
discriminator: string;
avatar: null;
status: Status;
avatar_url: string;
game?: Game;
}
export interface Game {
name: string;
}
export enum Status {
DND = 'dnd',
Idle = 'idle',
Online = 'online',
}
const COLUMN_COUNT = 3;
const discordServerId = assertSecret(import.meta.env.PUBLIC_DISCORD_SERVER_ID);
const widget = await fetch(
`https://discord.com/api/guilds/${discordServerId}/widget.json`,
).then((response) => response.json() as Promise<DiscordWidget>);
const filteredMembers = widget.members
.filter((member) => {
const isPrivateUsername =
member.username.length === 4 && member.username.endsWith('...');
return !isPrivateUsername;
})
.sort(() => Math.random() - 0.5);
const columnSize = Math.floor(filteredMembers.length / COLUMN_COUNT);
const columns = times(COLUMN_COUNT, (index) => {
const start = index * columnSize;
const end = start + columnSize;
return filteredMembers.slice(start, end);
});
function Member({
id,
avatar_url,
username,
status,
skeleton,
}: Member & MaybeSkeletonComponentProps) {
let statusColor: string;
if (skeleton) {
statusColor = Var('gray-9');
} else {
switch (status) {
case Status.DND:
statusColor = Var('red-9');
break;
case Status.Idle:
statusColor = Var('amber-9');
break;
case Status.Online:
statusColor = Var('jade-9');
break;
}
}
return (
<Text
style={{
maxWidth: '8rem',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
flexShrink: 0,
}}
color="gray"
>
<Flex key={id} align="center" gap="2" flexShrink="0" width="8rem">
<Box width="1.5em" height="1.5em" flexShrink="0" position="relative">
{!skeleton && (
<img
src={avatar_url}
alt={`${username} profile picture`}
style={{ width: '100%', height: '100%', borderRadius: '50%' }}
/>
)}
{skeleton && (
<Box
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
background: Var('gray-6'),
}}
/>
)}
<Box
width="0.5em"
height="0.5em"
position="absolute"
bottom="0"
right="0"
style={{ borderRadius: '50%', backgroundColor: statusColor }}
/>
</Box>
{skeleton && <InlineSkeleton style={{ flex: 1 }} />}
{!skeleton && username}
</Flex>
</Text>
);
}
function Scroller({
index,
skeleton,
}: { index: number } & MaybeSkeletonComponentProps) {
return (
<Flex gap="6" className={`scroller-${index}`} pt="2">
{columns.map((column, index) => (
<Flex direction="column" gap="2" key={index}>
{column.map((member) => (
<Member skeleton={skeleton} key={member.id} {...member} />
))}
</Flex>
))}
</Flex>
);
}
export function DiscordPlug({
locale,
skeleton,
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
const strings = getStrings(locale);
return (
<Flex justify="center" flexGrow="1" flexShrink="0">
<Link
style={{ display: 'contents' }}
href="https://discord.gg/nDt7AjGJQH"
target="_blank"
>
<Flex
direction="column"
maxWidth="26rem"
overflow="hidden"
style={{
borderRadius: Var('radius-2'),
}}
>
<Box
height="5rem"
style={{
perspective: '32rem',
background: `linear-gradient(90deg, ${Var('indigo-2')}, ${Var('blue-2')}, ${Var('purple-2')})`,
}}
position="relative"
overflow="hidden"
>
<Flex
top="-250%"
position="absolute"
direction="column"
width="fit-content"
height="32rem"
flexGrow="1"
overflow="hidden"
style={{
transform: 'rotateX(60deg) rotateZ(-20deg)',
}}
>
<Scroller skeleton={skeleton} index={0} />
<Scroller skeleton={skeleton} index={1} />
</Flex>
</Box>
<Flex
align="center"
justify="between"
p="3"
gap="4"
style={{
background: `linear-gradient(90deg, ${Var('indigo-3')}, ${Var('blue-3')}, ${Var('purple-3')})`,
}}
>
<Box display={{ initial: 'block', xs: 'none' }}>
<Text color="gray" highContrast>
{strings.website.home.discord.join_short}
</Text>
</Box>
<Box display={{ initial: 'none', xs: 'block' }}>
<Text color="gray" highContrast>
{strings.website.home.discord.join_long}
</Text>
</Box>
<Button color="iris">
{strings.website.home.discord.action}
<CaretRightIcon />
</Button>
</Flex>
</Flex>
</Link>
</Flex>
);
}
@@ -10,7 +10,14 @@ export function HomeLocaleSwitcher({
}: LocaleAcceptorProps) {
return (
<BlitzKitTheme>
<Flex justify="center" mt="4" py="6" align="center" gap="3">
<Flex
flexGrow="2"
flexShrink="0"
justify="center"
mt="4"
align="center"
gap="3"
>
<Text size="5" trim="end" color="gray">
<GlobeIcon width="1em" height="1em" />
</Text>
@@ -83,7 +83,7 @@ function Content() {
const { locale, strings } = useLocale();
return (
<Flex direction="column" pt="4" pb="8">
<Flex direction="column" pt="4" pb="6">
<Heading align="center" size="5">
{strings.website.home.hot_tanks.title}
</Heading>
@@ -0,0 +1,22 @@
import type { ComponentProps } from 'react';
export function MissingShellIcon(props: ComponentProps<'svg'>) {
return (
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M39.5461 16.5714L47.4286 24.4539M16.1429 37.5714L26.4286 47.8571M56 10.3802L53.6198 8C32.2378 20.0846 22.0873 28.6952 8 48.0661C8.13223 48.595 8.63471 50.2876 11.1736 52.8264C13.7124 55.3653 15.405 55.8678 15.9339 56C35.3048 41.9127 43.9154 31.7622 56 10.3802Z"
stroke="currentColor"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
+104 -155
View File
@@ -6,24 +6,14 @@ import {
GearIcon,
HamburgerMenuIcon,
} from '@radix-ui/react-icons';
import {
Box,
Code,
Flex,
Grid,
IconButton,
Link,
ScrollArea,
Separator,
Text,
} from '@radix-ui/themes';
import { Box, Code, Flex, IconButton, Link } from '@radix-ui/themes';
import { TOOLS } from '../constants/tools';
import { resolveBranchName } from '../core/blitzkit/branches';
import { homeTool, TOOLS } from '../constants/tools';
import { LinkI18n } from './LinkI18n';
import { getStrings } from '../core/i18n/getStrings';
import { Var } from '../core/radix/var';
import { LinkI18n } from './LinkI18n';
const toolsFiltered = TOOLS.filter((tool) => !('href' in tool));
const tools = [homeTool, ...toolsFiltered];
const { locale } = Astro.params;
const branchName = resolveBranchName(locale);
const isBranchNamed = branchName !== undefined;
@@ -31,93 +21,64 @@ const strings = getStrings(locale);
---
<style>
.navbar {
height: var(--navbar-height);
top: 0;
z-index: 1;
position: sticky;
}
.expander {
--navbar-height: 48px;
/* overflow: hidden; */
transition-duration: 250ms;
width: 100%;
backdrop-filter: blur(4rem) brightness(0.75);
-webkit-backdrop-filter: blur(4rem) brightness(0.75);
}
.expander[data-expanded='false'] {
height: var(--navbar-height);
}
.expander[data-expanded='false'] *[data-expanded] {
.navbar[data-expanded='false'] *[data-expanded] {
display: none;
}
.expander[data-expanded='true'] {
height: 100vh;
.navbar[data-expanded='true'] {
height: 100%;
}
.expander[data-expanded='true'] *[data-closed] {
.navbar[data-expanded='true'] *[data-closed] {
display: none;
}
.expander div[data-radix-scroll-area-viewport] {
overflow-y: scroll !important;
overscroll-behavior: contain;
}
.hamburger {
display: inline-flex;
}
.tools {
display: none;
}
@media (min-width: 768px) {
.hamburger {
display: none;
}
.tools {
display: flex;
}
.expander {
max-height: var(--navbar-height) !important;
}
}
</style>
<script>
const expander = document.querySelector<HTMLDivElement>('.expander');
const navbar = document.querySelector<HTMLDivElement>('.navbar');
const hamburger = document.querySelector<HTMLButtonElement>('.hamburger');
expander?.addEventListener('click', () => {
expander.dataset.expanded = 'false';
navbar?.addEventListener('click', () => {
navbar.dataset.expanded = 'false';
});
hamburger?.addEventListener('click', (event) => {
if (!expander) return;
if (!navbar) return;
event.stopPropagation();
expander.dataset.expanded = `${expander.dataset.expanded === 'false'}`;
navbar.dataset.expanded = `${navbar.dataset.expanded === 'false'}`;
});
</script>
<Flex className="navbar">
<Flex className="expander" data-expanded="false" direction="column" gap="2">
<Flex justify="center">
<Flex flexGrow="1" maxWidth="1024px" p="3" align="center">
<Box height={{ initial: '3.5rem', sm: '3rem' }} />
<Flex
direction="column"
align="center"
data-expanded="false"
className="navbar"
width="100%"
top="0"
position="fixed"
style={{
zIndex: 1,
}}
>
<Flex
width="100%"
p={{ initial: '4', sm: '3' }}
direction="column"
align="center"
style={{ backgroundColor: Var('gray-1') }}
>
<Flex gap="4" direction="column" maxWidth="80rem" width="100%">
<Flex align="center">
<LinkI18n
{locale}
color="gray"
highContrast
href="/"
underline="hover"
weight="bold"
weight="medium"
>
<Flex align="center" gap="1">
BlitzKit
@@ -133,7 +94,13 @@ const strings = getStrings(locale);
<Box flexGrow="1" />
<Flex align="center" gap="2" justify="center" className="tools">
<Flex
align="center"
gap={{ initial: '4', md: '6' }}
display={{ initial: 'none', sm: 'flex' }}
justify="center"
className="tools"
>
{
toolsFiltered.map((tool, index) => {
const unavailableOnBranch = tool.branches?.every(
@@ -150,19 +117,17 @@ const strings = getStrings(locale);
if (unavailableOnBranch) return null;
return (
<Fragment key={tool.id}>
{index > 0 && <Separator orientation="vertical" size="1" />}
<LinkI18n
{locale}
color="gray"
highContrast={selected}
size="2"
href={`/${tool.id}`}
underline={selected ? 'always' : 'hover'}
>
{toolStrings.name}
</LinkI18n>
</Fragment>
<LinkI18n
{locale}
weight={selected ? 'medium' : 'regular'}
color="gray"
highContrast={selected}
size="2"
href={`/${tool.id}`}
underline="hover"
>
{toolStrings.name}
</LinkI18n>
);
})
}
@@ -170,7 +135,7 @@ const strings = getStrings(locale);
<Box flexGrow="1" />
<Flex align="center" gap="3">
<Flex align="center" gap="4">
<Link
style={{ display: 'flex', alignContent: 'center' }}
color="gray"
@@ -178,7 +143,7 @@ const strings = getStrings(locale);
underline="none"
target="_blank"
>
<DiscordLogoIcon />
<DiscordLogoIcon width="1em" height="1em" />
</Link>
<LinkI18n
@@ -188,100 +153,84 @@ const strings = getStrings(locale);
href="/settings"
underline="none"
>
<GearIcon />
<GearIcon width="1em" height="1em" />
</LinkI18n>
<!-- <NavbarAccount client:idle /> -->
<IconButton variant="ghost" color="gray" className="hamburger">
<Cross1Icon data-expanded />
<HamburgerMenuIcon data-closed />
</IconButton>
<Flex align="center" display={{ initial: 'flex', sm: 'none' }}>
<IconButton variant="ghost" color="gray" className="hamburger">
<Cross1Icon data-expanded />
<HamburgerMenuIcon data-closed />
</IconButton>
</Flex>
</Flex>
</Flex>
</Flex>
<ScrollArea scrollbars="vertical" style={{ height: '100%', width: '100%' }}>
<Grid
columns="repeat(auto-fill, minmax(12rem, 1fr))"
flow="dense"
gap="2"
width="100%"
p="3"
pt="0"
<Flex
direction="column"
data-expanded
gap="3"
display={{ initial: 'flex', sm: 'none' }}
>
{
tools.map((tool) => {
toolsFiltered.map((tool) => {
const unavailableOnBranch = tool.branches?.every(
(branch) =>
branch !== assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
);
if (unavailableOnBranch) return null;
const toolStrings =
strings.website.tools[
(tool.stringsId ??
tool.id) as keyof typeof strings.website.tools
];
if (unavailableOnBranch) return null;
return (
<Flex
key={tool.id}
style={{
position: 'relative',
borderRadius: 'var(--radius-2)',
overflow: 'hidden',
backgroundImage: `url(${imgur(tool.image, {
format: 'jpeg',
size: ImgurSize.MediumThumbnail,
})})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
aspectRatio: '2 / 1',
}}
<Link
color="gray"
highContrast
style={{ display: 'contents' }}
href={`/tools/${tool.id}`}
>
<LinkI18n
{locale}
href={tool.id.length === 0 ? '/' : `/${tool.id}`}
<Box
style={{
display: 'flex',
width: '100%',
height: '100%',
flexDirection: 'column',
textDecoration: 'none',
color: 'inherit',
borderRadius: Var('radius-2'),
backgroundImage: `url(/assets/images/tools/${tool.id}.webp)`,
backgroundSize: '200%',
backgroundPosition: 'right center',
}}
>
<Box
<Flex
py="4"
justify="end"
flexGrow="1"
style={{
backgroundImage: `url(${imgur(tool.image, {
format: 'jpeg',
size: ImgurSize.MediumThumbnail,
})})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<Flex
p="2"
align="center"
width="100%"
style={{
backgroundColor: 'var(--color-panel-translucent)',
backdropFilter: 'blur(4rem)',
WebkitBackdropFilter: 'blur(4rem)',
background: `linear-gradient(90deg, transparent, ${Var('gray-1')})`,
}}
>
<Text>{toolStrings.name}</Text>
{toolStrings.name}
</Flex>
</LinkI18n>
</Flex>
</Box>
</Link>
);
})
}
</Grid>
</ScrollArea>
</Flex>
</Flex>
</Flex>
<Box
data-expanded
flexGrow="1"
width="100%"
display={{ initial: 'block', sm: 'none' }}
style={{
backgroundColor: Var('black-a7'),
backdropFilter: 'blur(0.5rem)',
WebkitBackdropFilter: 'blur(0.5rem)',
}}
/>
</Flex>
@@ -39,7 +39,7 @@ export function PageWrapper({
gap="4"
p={padding}
width="100%"
maxWidth="50rem"
maxWidth="80rem"
style={{
margin: 'auto',
boxSizing: 'border-box',
+6 -1
View File
@@ -10,9 +10,14 @@ const PUBLIC_GOOGLE_TAG_MANAGER_ID = assertSecret(
<script
is:inline
async
type="text/partytown"
src={`https://www.googletagmanager.com/gtag/js?id=${PUBLIC_GOOGLE_TAG_MANAGER_ID}`}
></script>
<script is:inline define:vars={{ PUBLIC_GOOGLE_TAG_MANAGER_ID }}>
<script
is:inline
define:vars={{ PUBLIC_GOOGLE_TAG_MANAGER_ID }}
type="text/partytown"
>
window.dataLayer = window.dataLayer || [];
function gtag() {
@@ -1,15 +1,30 @@
import {
asset,
ShellType,
TANK_CLASSES,
TankType,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { ReloadIcon } from '@radix-ui/react-icons';
import { Box, Flex, IconButton, Text } from '@radix-ui/themes';
import {
Box,
Button,
Flex,
IconButton,
Inset,
Popover,
Text,
} from '@radix-ui/themes';
import { times } from 'lodash-es';
import { awaitableGameDefinitions } from '../../../core/awaitables/gameDefinitions';
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
import { Var } from '../../../core/radix/var';
import { useLocale } from '../../../hooks/useLocale';
import {
$tankFilters,
initialTankFilters,
type TankFilters,
} from '../../../stores/tankFilters';
import {
$tankopediaSort,
initialTankopediaSort,
@@ -19,6 +34,7 @@ import { ExperimentIcon } from '../../ExperimentIcon';
import { GunAutoloaderIcon } from '../../GunAutoloaderIcon';
import { GunAutoreloaderIcon } from '../../GunAutoreloaderIcon';
import { GunRegularIcon } from '../../GunRegularIcon';
import { MissingShellIcon } from '../../MissingShellIcon';
import { ResearchedIcon } from '../../ResearchedIcon';
import { ScienceIcon } from '../../ScienceIcon';
import { ScienceOffIcon } from '../../ScienceOffIcon';
@@ -29,11 +45,90 @@ interface FilterControlProps {
const gameDefinitions = await awaitableGameDefinitions;
export function FilterControl({ compact }: FilterControlProps) {
const shellTypeIcons: Record<ShellType, string> = {
[ShellType.AP]: 'ap',
[ShellType.APCR]: 'ap_cr',
[ShellType.HE]: 'he',
[ShellType.HEAT]: 'hc',
};
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
const tankFilters = useStore($tankFilters);
return (
<Flex height="fit-content" gap="2" align="start" justify="center">
<Popover.Root>
<Popover.Trigger>
<IconButton variant="soft" radius="none" color="gray" highContrast>
{tankFilters.shells[index] === null && (
<Text color="gray" style={{ display: 'contents' }}>
<MissingShellIcon width="1em" height="1em" />
</Text>
)}
{tankFilters.shells[index] !== null && (
<img
style={{ width: '1em', height: '1em' }}
src={asset(
`icons/shells/${shellTypeIcons[tankFilters.shells[index]]}${premium ? '_premium' : ''}.webp`,
)}
/>
)}
</IconButton>
</Popover.Trigger>
<Popover.Content>
<Inset>
<Flex>
{Object.values(ShellType).map((shellType) => {
if (typeof shellType === 'string') return null;
const selected = tankFilters.shells[index] === shellType;
return (
<IconButton
value={`${shellType}`}
radius="none"
variant={selected ? 'solid' : 'soft'}
onClick={() => {
const mutated = [
tankFilters.shells[0],
tankFilters.shells[1],
tankFilters.shells[2],
] as TankFilters['shells'];
mutated[index] = selected ? null : shellType;
$tankFilters.setKey('shells', mutated);
}}
highContrast={selected}
color={selected ? undefined : 'gray'}
>
<img
src={asset(
`icons/shells/${shellTypeIcons[shellType]}${premium ? '_premium' : ''}.webp`,
)}
style={{ width: '1em', height: '1em' }}
/>
</IconButton>
);
})}
</Flex>
</Inset>
</Popover.Content>
</Popover.Root>
);
}
export function FilterControl({ compact }: FilterControlProps) {
const tankFilters = useStore($tankFilters);
const { strings } = useLocale();
return (
<Flex
height="fit-content"
gap="2"
align={{ initial: 'start', md: 'center' }}
justify="center"
>
<Flex
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
overflow="hidden"
@@ -67,6 +162,15 @@ export function FilterControl({ compact }: FilterControlProps) {
);
})}
</Flex>
<Box
display={{ initial: 'none', md: 'block' }}
style={{
height: Var('space-1'),
backgroundColor: Var('gray-a3'),
}}
/>
<Flex direction={compact ? 'column' : { md: 'row', initial: 'column' }}>
{times(5, (index) => {
const tier = index + 6;
@@ -137,6 +241,15 @@ export function FilterControl({ compact }: FilterControlProps) {
);
})}
</Flex>
<Box
display={{ initial: 'none', md: 'block' }}
style={{
height: Var('space-1'),
backgroundColor: Var('gray-a3'),
}}
/>
<Flex direction={compact ? 'column' : { md: 'row', initial: 'column' }}>
{gameDefinitions.nations
.slice(Math.ceil(gameDefinitions.nations.length / 2))
@@ -176,270 +289,315 @@ export function FilterControl({ compact }: FilterControlProps) {
</Flex>
<Flex
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
direction={{ initial: 'row', md: 'column' }}
align="start"
gap={{ initial: '2', md: '1' }}
>
{TANK_CLASSES.map((tankClass) => {
const Icon = classIcons[tankClass];
const selected = tankFilters.classes?.includes(tankClass);
<Flex gap="2" align="start">
<Flex
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
>
{TANK_CLASSES.map((tankClass) => {
const Icon = classIcons[tankClass];
const selected = tankFilters.classes?.includes(tankClass);
return (
return (
<IconButton
key={tankClass}
variant={selected ? 'solid' : 'soft'}
radius="none"
color={selected ? undefined : 'gray'}
highContrast
onClick={() => {
if (tankFilters.classes.includes(tankClass)) {
$tankFilters.setKey(
'classes',
tankFilters.classes.filter((c) => c !== tankClass),
);
} else {
$tankFilters.setKey('classes', [
...tankFilters.classes,
tankClass,
]);
}
}}
>
<Icon style={{ width: '1em', height: '1em' }} />
</IconButton>
);
})}
</Flex>
<Flex
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
>
<IconButton
key={tankClass}
variant={selected ? 'solid' : 'soft'}
variant={
tankFilters.gunType.includes('regular') ? 'solid' : 'soft'
}
radius="none"
color={selected ? undefined : 'gray'}
color={
tankFilters.gunType.includes('regular') ? undefined : 'gray'
}
highContrast
onClick={() => {
if (tankFilters.classes.includes(tankClass)) {
if (tankFilters.gunType.includes('regular')) {
$tankFilters.setKey(
'classes',
tankFilters.classes.filter((c) => c !== tankClass),
'gunType',
tankFilters.gunType.filter((t) => t !== 'regular'),
);
} else {
$tankFilters.setKey('classes', [
...tankFilters.classes,
tankClass,
$tankFilters.setKey('gunType', [
...tankFilters.gunType,
'regular',
]);
}
}}
>
<Icon style={{ width: '1em', height: '1em' }} />
<GunRegularIcon style={{ width: '1em', height: '1em' }} />
</IconButton>
);
})}
</Flex>
<IconButton
variant={
tankFilters.gunType.includes('auto_loader') ? 'solid' : 'soft'
}
radius="none"
color={
tankFilters.gunType.includes('auto_loader') ? undefined : 'gray'
}
highContrast
onClick={() => {
if (tankFilters.gunType.includes('auto_loader')) {
$tankFilters.setKey(
'gunType',
tankFilters.gunType.filter((t) => t !== 'auto_loader'),
);
} else {
$tankFilters.setKey('gunType', [
...tankFilters.gunType,
'auto_loader',
]);
}
}}
>
<GunAutoloaderIcon style={{ width: '1em', height: '1em' }} />
</IconButton>
<IconButton
variant={
tankFilters.gunType.includes('auto_reloader') ? 'solid' : 'soft'
}
radius="none"
color={
tankFilters.gunType.includes('auto_reloader')
? undefined
: 'gray'
}
highContrast
onClick={() => {
if (tankFilters.gunType.includes('auto_reloader')) {
$tankFilters.setKey(
'gunType',
tankFilters.gunType.filter((t) => t !== 'auto_reloader'),
);
} else {
$tankFilters.setKey('gunType', [
...tankFilters.gunType,
'auto_reloader',
]);
}
}}
>
<GunAutoreloaderIcon style={{ width: '1em', height: '1em' }} />
</IconButton>
</Flex>
<Flex
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
>
<IconButton
variant={tankFilters.gunType.includes('regular') ? 'solid' : 'soft'}
radius="none"
color={tankFilters.gunType.includes('regular') ? undefined : 'gray'}
highContrast
onClick={() => {
if (tankFilters.gunType.includes('regular')) {
$tankFilters.setKey(
'gunType',
tankFilters.gunType.filter((t) => t !== 'regular'),
);
} else {
$tankFilters.setKey('gunType', [
...tankFilters.gunType,
'regular',
]);
}
}}
>
<GunRegularIcon style={{ width: '1em', height: '1em' }} />
</IconButton>
<IconButton
variant={
tankFilters.gunType.includes('auto_loader') ? 'solid' : 'soft'
}
radius="none"
color={
tankFilters.gunType.includes('auto_loader') ? undefined : 'gray'
}
highContrast
onClick={() => {
if (tankFilters.gunType.includes('auto_loader')) {
$tankFilters.setKey(
'gunType',
tankFilters.gunType.filter((t) => t !== 'auto_loader'),
);
} else {
$tankFilters.setKey('gunType', [
...tankFilters.gunType,
'auto_loader',
]);
}
}}
>
<GunAutoloaderIcon style={{ width: '1em', height: '1em' }} />
</IconButton>
<IconButton
variant={
tankFilters.gunType.includes('auto_reloader') ? 'solid' : 'soft'
}
radius="none"
color={
tankFilters.gunType.includes('auto_reloader') ? undefined : 'gray'
}
highContrast
onClick={() => {
if (tankFilters.gunType.includes('auto_reloader')) {
$tankFilters.setKey(
'gunType',
tankFilters.gunType.filter((t) => t !== 'auto_reloader'),
);
} else {
$tankFilters.setKey('gunType', [
...tankFilters.gunType,
'auto_reloader',
]);
}
}}
>
<GunAutoreloaderIcon style={{ width: '1em', height: '1em' }} />
</IconButton>
</Flex>
<Box display={{ initial: 'none', md: 'block' }}>
<Button
color="red"
onClick={() => {
$tankFilters.set(initialTankFilters);
$tankopediaSort.set(initialTankopediaSort);
}}
>
<ReloadIcon />
{strings.website.common.tank_search.clear_filters}
</Button>
</Box>
</Flex>
<Flex
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
>
<IconButton
variant={
tankFilters.types?.includes(TankType.RESEARCHABLE)
? 'solid'
: 'soft'
}
radius="none"
color={
tankFilters.types?.includes(TankType.RESEARCHABLE)
? undefined
: 'gray'
}
highContrast
onClick={() => {
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
$tankFilters.setKey(
'types',
tankFilters.types.filter((t) => t !== TankType.RESEARCHABLE),
);
} else {
$tankFilters.setKey('types', [
...tankFilters.types,
TankType.RESEARCHABLE,
]);
}
}}
>
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
</IconButton>
<IconButton
variant={
tankFilters.types?.includes(TankType.PREMIUM) ? 'solid' : 'soft'
}
radius="none"
color={
tankFilters.types?.includes(TankType.PREMIUM) ? 'amber' : 'gray'
}
highContrast
onClick={() => {
if (tankFilters.types.includes(TankType.PREMIUM)) {
$tankFilters.setKey(
'types',
tankFilters.types.filter((t) => t !== TankType.PREMIUM),
);
} else {
$tankFilters.setKey('types', [
...tankFilters.types,
TankType.PREMIUM,
]);
}
}}
>
<Text
color={
tankFilters.types?.includes(TankType.PREMIUM)
? undefined
: 'amber'
}
style={{ display: 'flex', justifyContent: 'center' }}
<Flex gap="2" align="start">
<Flex
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
>
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
</Text>
</IconButton>
<IconButton
variant={
tankFilters.types?.includes(TankType.COLLECTOR) ? 'solid' : 'soft'
}
radius="none"
color={
tankFilters.types?.includes(TankType.COLLECTOR) ? 'blue' : 'gray'
}
highContrast
onClick={() => {
if (tankFilters.types.includes(TankType.COLLECTOR)) {
$tankFilters.setKey(
'types',
tankFilters.types.filter((t) => t !== TankType.COLLECTOR),
);
} else {
$tankFilters.setKey('types', [
...tankFilters.types,
TankType.COLLECTOR,
]);
}
}}
>
<Text
color={
tankFilters.types?.includes(TankType.COLLECTOR)
? undefined
: 'blue'
}
style={{ display: 'flex', justifyContent: 'center' }}
<IconButton
variant={
tankFilters.types?.includes(TankType.RESEARCHABLE)
? 'solid'
: 'soft'
}
radius="none"
color={
tankFilters.types?.includes(TankType.RESEARCHABLE)
? undefined
: 'gray'
}
highContrast
onClick={() => {
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
$tankFilters.setKey(
'types',
tankFilters.types.filter(
(t) => t !== TankType.RESEARCHABLE,
),
);
} else {
$tankFilters.setKey('types', [
...tankFilters.types,
TankType.RESEARCHABLE,
]);
}
}}
>
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
</IconButton>
<IconButton
variant={
tankFilters.types?.includes(TankType.PREMIUM) ? 'solid' : 'soft'
}
radius="none"
color={
tankFilters.types?.includes(TankType.PREMIUM) ? 'amber' : 'gray'
}
highContrast
onClick={() => {
if (tankFilters.types.includes(TankType.PREMIUM)) {
$tankFilters.setKey(
'types',
tankFilters.types.filter((t) => t !== TankType.PREMIUM),
);
} else {
$tankFilters.setKey('types', [
...tankFilters.types,
TankType.PREMIUM,
]);
}
}}
>
<Text
color={
tankFilters.types?.includes(TankType.PREMIUM)
? undefined
: 'amber'
}
style={{ display: 'flex', justifyContent: 'center' }}
>
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
</Text>
</IconButton>
<IconButton
variant={
tankFilters.types?.includes(TankType.COLLECTOR)
? 'solid'
: 'soft'
}
radius="none"
color={
tankFilters.types?.includes(TankType.COLLECTOR)
? 'blue'
: 'gray'
}
highContrast
onClick={() => {
if (tankFilters.types.includes(TankType.COLLECTOR)) {
$tankFilters.setKey(
'types',
tankFilters.types.filter((t) => t !== TankType.COLLECTOR),
);
} else {
$tankFilters.setKey('types', [
...tankFilters.types,
TankType.COLLECTOR,
]);
}
}}
>
<Text
color={
tankFilters.types?.includes(TankType.COLLECTOR)
? undefined
: 'blue'
}
style={{ display: 'flex', justifyContent: 'center' }}
>
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
</Text>
</IconButton>
</Flex>
<Flex
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
>
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
</Text>
</IconButton>
<IconButton
variant={tankFilters.testing === 'include' ? 'solid' : 'soft'}
radius="none"
color={tankFilters.testing === 'include' ? undefined : 'gray'}
highContrast
onClick={() => {
$tankFilters.setKey('testing', 'include');
}}
>
<ExperimentIcon
style={{ width: '1em', height: '1em', color: 'currentColor' }}
/>
</IconButton>
<IconButton
variant={tankFilters.testing === 'exclude' ? 'solid' : 'soft'}
radius="none"
color={tankFilters.testing === 'exclude' ? undefined : 'gray'}
highContrast
onClick={() => {
$tankFilters.setKey('testing', 'exclude');
}}
>
<ScienceOffIcon
style={{ width: '1em', height: '1em', color: 'currentColor' }}
/>
</IconButton>
<IconButton
variant={tankFilters.testing === 'only' ? 'solid' : 'soft'}
radius="none"
color={tankFilters.testing === 'only' ? undefined : 'gray'}
highContrast
onClick={() => {
$tankFilters.setKey('testing', 'only');
}}
>
<ScienceIcon
style={{ width: '1em', height: '1em', color: 'currentColor' }}
/>
</IconButton>
</Flex>
<Flex
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
>
<ShellFilter index={0} />
<ShellFilter index={1} premium />
<ShellFilter index={2} />
</Flex>
</Flex>
</Flex>
<Flex
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
>
<IconButton
variant={tankFilters.testing === 'include' ? 'solid' : 'soft'}
radius="none"
color={tankFilters.testing === 'include' ? undefined : 'gray'}
highContrast
onClick={() => {
$tankFilters.setKey('testing', 'include');
}}
>
<ExperimentIcon
style={{ width: '1em', height: '1em', color: 'currentColor' }}
/>
</IconButton>
<IconButton
variant={tankFilters.testing === 'exclude' ? 'solid' : 'soft'}
radius="none"
color={tankFilters.testing === 'exclude' ? undefined : 'gray'}
highContrast
onClick={() => {
$tankFilters.setKey('testing', 'exclude');
}}
>
<ScienceOffIcon
style={{ width: '1em', height: '1em', color: 'currentColor' }}
/>
</IconButton>
<IconButton
variant={tankFilters.testing === 'only' ? 'solid' : 'soft'}
radius="none"
color={tankFilters.testing === 'only' ? undefined : 'gray'}
highContrast
onClick={() => {
$tankFilters.setKey('testing', 'only');
}}
>
<ScienceIcon
style={{ width: '1em', height: '1em', color: 'currentColor' }}
/>
</IconButton>
</Flex>
<Box>
<Box display={{ initial: 'block', md: 'none' }} ml="4">
<IconButton
color="red"
onClick={() => {
@@ -1,4 +1,4 @@
import { imgur, ShellType } from '@blitzkit/core';
import { ShellType } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n/src/literals';
import {
Button,
@@ -42,7 +42,7 @@ export function CustomShellButton() {
>
<img
alt="custom shell"
src={imgur('j2CoXak')}
src="/assets/images/icons/custom-shell.png"
style={{
width: '50%',
height: '50%',
@@ -1,4 +1,3 @@
import { imgur } from '@blitzkit/core';
import { Box, Flex, SegmentedControl, Tooltip } from '@radix-ui/themes';
import { useState, type RefObject } from 'react';
import { useFullScreen } from '../../../../../hooks/useFullScreen';
@@ -65,7 +64,7 @@ export function Options({ canvas }: OptionsProps) {
left="50%"
style={{
zIndex: 2,
backgroundImage: `url(${imgur('27Gwth4')})`,
backgroundImage: 'url(/assets/images/icons/aim-caret.png)',
backgroundSize: 'contain',
transform: 'translateX(-50%)',
borderRadius: '50%',
@@ -373,7 +372,10 @@ export function Options({ canvas }: OptionsProps) {
<SegmentedControl.Item value={`${TankopediaDisplay.Model}`}>
<Tooltip content={strings.website.tools.tanks.sandbox.model.name}>
<Flex height="100%" align="center">
<img src={imgur('jAdYf0m')} style={{ height: '1.25em' }} />
<img
src="/assets/images/icons/tankopedia-model.png"
style={{ height: '1.25em' }}
/>
</Flex>
</Tooltip>
</SegmentedControl.Item>
@@ -382,7 +384,10 @@ export function Options({ canvas }: OptionsProps) {
content={strings.website.tools.tanks.sandbox.dynamic.name}
>
<Flex height="100%" align="center">
<img src={imgur('oe4Cq0g')} style={{ height: '1.25em' }} />
<img
src="/assets/images/icons/tankopedia-dynamic-armor.png"
style={{ height: '1.25em' }}
/>
</Flex>
</Tooltip>
</SegmentedControl.Item>
@@ -391,7 +396,10 @@ export function Options({ canvas }: OptionsProps) {
content={strings.website.tools.tanks.sandbox.static.name}
>
<Flex height="100%" align="center">
<img src={imgur('VQ4uDno')} style={{ height: '1.25em' }} />
<img
src="/assets/images/icons/tankopedia-static-armor.png"
style={{ height: '1.25em' }}
/>
</Flex>
</Tooltip>
</SegmentedControl.Item>
@@ -400,14 +408,29 @@ export function Options({ canvas }: OptionsProps) {
content={strings.website.tools.tanks.sandbox.dissector.name}
>
<Flex height="100%" align="center">
<img src={imgur('ErJEWbY')} style={{ height: '1.25em' }} />
<img
src="/assets/images/icons/tankopedia-static-armor.png"
style={{ height: '1.25em' }}
/>
</Flex>
</Tooltip>
</SegmentedControl.Item>
<SegmentedControl.Item value={`${TankopediaDisplay.Dissector}`}>
<Tooltip
content={strings.website.tools.tanks.sandbox.dissector.name}
>
<Flex height="100%" align="center">
<img
src="/assets/images/icons/tankopedia-dissector.png"
style={{ height: '1.25em' }}
/>
</Flex>
</Tooltip>
</SegmentedControl.Item>
{/* <SegmentedControl.Item value={`${TankopediaDisplay.ShootingRange}`}>
<Tooltip content="Shooting range">
<Flex height="100%" align="center">
<img src={imgur('q8ZPfzG')} style={{ height: '1.25em' }} />
<img src="/assets/images/icons/tankopedia-shooting-range.png" style={{ height: '1.25em' }} />
</Flex>
</Tooltip>
</SegmentedControl.Item> */}
@@ -1,4 +1,3 @@
import { imgur } from '@blitzkit/core';
import { Box, type BoxProps } from '@radix-ui/themes';
import { forwardRef } from 'react';
import { Var } from '../../../../../../../../core/radix/var';
@@ -56,7 +55,7 @@ export const TargetCircle = forwardRef<HTMLDivElement, TargetCircleProps>(
top="50%"
left="50%"
style={{
backgroundImage: `url(${imgur('27Gwth4')})`,
backgroundImage: 'url(/assets/images/icons/aim-caret.png)',
backgroundSize: 'contain',
transform: 'translateX(-50%)',
borderRadius: '50%',
@@ -1,4 +1,4 @@
import { imgur, J_HAT } from '@blitzkit/core';
import { J_HAT } from '@blitzkit/core';
import { useLoader } from '@react-three/fiber';
import { useCallback, useEffect, useRef } from 'react';
import { Group, Plane, TextureLoader, Vector2 } from 'three';
@@ -89,8 +89,11 @@ export function SceneProps() {
// let { dispersion } = characteristics;
// const gunRotationSpeed = degToRad(characteristics.gunTraverseSpeed);
// const mockTank = useLoader(GLTFLoader, asset(`3d/tanks/models/6929.glb`));
const gridTexture = useLoader(TextureLoader, imgur('iq0iOCJ'));
const dragTexture = useLoader(TextureLoader, imgur('Vi3xXsb'));
const gridTexture = useLoader(TextureLoader, '/assets/images/3d/grid.png');
const dragTexture = useLoader(
TextureLoader,
'/assets/images/3d/drag-slicer.png',
);
gridTexture.anisotropy = 2;
// const path = new Vector3();
// const direction = new Vector3();
@@ -1,4 +1,4 @@
import { asset, imgur, TankType } from '@blitzkit/core';
import { asset, 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';
@@ -140,7 +140,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
<Flex
key={multiplier}
style={{
background: `url(${imgur('7hDltb4')})`,
background: 'url(/assets/images/icons/xp-multiplier.png)',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
+3 -13
View File
@@ -1,5 +1,5 @@
---
import { assertSecret, imgur, ImgurSize } from '@blitzkit/core';
import { assertSecret } from '@blitzkit/core';
import { CaretRightIcon } from '@radix-ui/react-icons';
import { Box, Button, Code, Flex, Heading, Text } from '@radix-ui/themes';
import type { Tool } from '../constants/tools';
@@ -48,12 +48,7 @@ const toolStrings =
</style>
<Flex
style={{
backgroundImage: `url(${imgur(tool.image, {
format: 'jpeg',
size: ImgurSize.SmallThumbnail,
})})`,
}}
style={{ backgroundImage: `url(/assets/images/tools/${tool.id}.webp)` }}
data-disabled={disabled}
data-significant={tool.significant}
className="container"
@@ -72,12 +67,7 @@ const toolStrings =
<Box
flexGrow="1"
style={{
backgroundImage: `url(${imgur(tool.image, {
format: 'jpeg',
size: tool.significant
? ImgurSize.HugeThumbnail
: ImgurSize.LargeThumbnail,
})})`,
backgroundImage: `url(/assets/images/tools/${tool.id}.webp)`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
@@ -1,5 +1,5 @@
---
import { imgur } from '@blitzkit/core';
import { AstroSeo } from '@astrolib/seo';
import { type Tool } from '../constants/tools';
import { getStrings } from '../core/i18n/getStrings';
@@ -16,7 +16,18 @@ const toolStrings =
];
---
<title>{toolStrings.name}</title>
<meta property="og:image" content={imgur(tool.image)} />
<meta name="description" content={toolStrings.seo_description} />
<meta property="og:description" content={toolStrings.seo_description} />
<AstroSeo
title={toolStrings.name}
description={toolStrings.seo_description}
canonical={Astro.url.href}
openGraph={{
url: Astro.url.href,
images: [
{ url: `${Astro.url.origin}/assets/images/tools/${tool.id}.webp` },
],
title: toolStrings.name,
description: toolStrings.seo_description,
site_name: 'BlitzKit',
}}
twitter={{ cardType: 'summary_large_image' }}
/>
-13
View File
@@ -4,7 +4,6 @@ import type { ComponentProps } from 'react';
export interface Tool {
id: string;
stringsId?: string;
image: string;
disabled?: boolean;
href?: string;
button: {
@@ -17,7 +16,6 @@ export interface Tool {
export const tanksTool: Tool = {
id: 'tanks',
image: 'D8j6GZx',
button: {
color: 'purple',
},
@@ -36,7 +34,6 @@ export const tanksTool: Tool = {
export const compareTool: Tool = {
id: 'compare',
image: 'O6VNl6e',
button: {
color: 'crimson',
},
@@ -46,7 +43,6 @@ export const compareTool: Tool = {
export const sessionTool: Tool = {
// TODO: rename to tracker
id: 'session',
image: 'HdG9sTf',
button: {
color: 'blue',
},
@@ -56,7 +52,6 @@ export const sessionTool: Tool = {
export const discordTool: Tool = {
id: 'discord',
href: 'https://discord.com/application-directory/1097673957865443370',
image: '0bWE5hC',
button: {
color: 'indigo',
},
@@ -65,7 +60,6 @@ export const discordTool: Tool = {
export const moreTool: Tool = {
id: 'more',
href: 'https://discord.gg/nDt7AjGJQH',
image: '1nPm6VI',
button: {
color: 'plum',
},
@@ -76,7 +70,6 @@ export const playersTool: Tool = {
button: {
color: 'blue',
},
image: 'YelSOfT',
href: '_', // TODO: remove this to re-enable link
branches: ['main', 'dev'],
};
@@ -86,7 +79,6 @@ export const performanceTool: Tool = {
button: {
color: 'jade',
},
image: 'vOKFB03',
branches: ['main', 'dev'],
};
@@ -95,14 +87,12 @@ export const embedTool: Tool = {
button: {
color: 'red',
},
image: 'Q0faRYg',
branches: ['main', 'dev'],
};
export const homeTool: Tool = {
id: 'home',
button: { color: 'blue' },
image: 'rUPie9G',
};
export const galleryTool: Tool = {
@@ -110,7 +100,6 @@ export const galleryTool: Tool = {
button: {
color: 'gold',
},
image: 'VhKlknk',
branches: ['main', 'dev'],
};
@@ -120,7 +109,6 @@ export const tierListTool: Tool = {
button: {
color: 'orange',
},
image: 'oKKc8Xv',
branches: ['main', 'dev', 'opentest', 'preview'],
};
@@ -129,7 +117,6 @@ export const chartsTool: Tool = {
button: {
color: 'bronze',
},
image: '6fhkljA',
branches: ['main', 'dev', 'opentest', 'preview'],
};
@@ -1,6 +1,9 @@
import type { TankDefinition } from '@blitzkit/core';
import { times } from 'lodash-es';
import type { TankFilters } from '../../stores/tankFilters';
const SHELLS = times(3, (index) => index);
export function filterTank(filters: TankFilters, tank: TankDefinition) {
return (
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
@@ -22,6 +25,17 @@ export function filterTank(filters: TankFilters, tank: TankDefinition) {
(filters.gunType.includes('auto_reloader') &&
tank.turrets.some((turret) =>
turret.guns.some((gun) => gun.gun_type!.$case === 'auto_reloader'),
)))
))) &&
tank.turrets.some((turret) =>
turret.guns.some((gun) =>
SHELLS.every(
(index) =>
filters.shells[index] === null ||
gun.gun_type!.value.base.shells[index] === undefined ||
gun.gun_type!.value.base.shells[index].type ===
filters.shells[index],
),
),
)
);
}
+1 -1
View File
@@ -9,7 +9,7 @@ export type VarName =
| `radius-${1 | 2 | 3 | 4 | 5 | 6 | 'full'}`
| `space-${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`
| `shadow-${1 | 2 | 3 | 4 | 5 | 6}`
| `${RadixColor | 'accent' | 'gray'}-${
| `${RadixColor | 'accent' | 'gray' | 'black' | 'white'}-${
| `${'' | 'a'}${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12}`
| 'surface'
| 'indicator'
@@ -29,7 +29,6 @@ const locale = Astro.params.locale ?? DEFAULT_LOCALE;
<script>
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
import { getBestLocale } from '../core/i18n/getBestLocale';
const isAPI = window.location.pathname.startsWith('/api');
const isI18nPath = SUPPORTED_LOCALES.some((locale) =>
window.location.pathname.startsWith(`/${locale}`),
@@ -1,5 +1,4 @@
---
import { imgur, ImgurSize } from '@blitzkit/core';
import { Box, Flex, Heading, Text } from '@radix-ui/themes';
import ToolOpenGraph from '../components/ToolOpenGraph.astro';
import { type Tool } from '../constants/tools';
@@ -24,10 +23,7 @@ const toolStrings =
<BaseLayout>
<Box
style={{
backgroundImage: `url(${imgur(tool!.image, {
format: 'jpeg',
size: ImgurSize.SmallThumbnail,
})})`,
backgroundImage: `url(/assets/images/tools/${tool.id}.webp)`,
backgroundSize: 'cover',
}}
>
@@ -1,4 +1,5 @@
---
import { AstroSeo } from '@astrolib/seo';
import type { GetStaticPaths } from 'astro';
import { readFile } from 'fs/promises';
import BaseLayout from '../../../../../layouts/BaseLayout.astro';
@@ -7,7 +8,6 @@ import { Page } from './_index';
export const getStaticPaths = (async () => {
const parentPaths = parentGetStaticPaths();
const ROOT = '../../../../../../../../docs/';
const files = import.meta.glob('../../../../../../../../docs/**/*.md', {
eager: true,
@@ -40,7 +40,18 @@ const { content } = Astro.props;
const title = content.split('\n')[0].replace('# ', '');
---
<title>{title}</title>
<AstroSeo
title={title}
description={title}
canonical={Astro.url.href}
openGraph={{
url: Astro.url.href,
title,
description: title,
site_name: 'BlitzKit',
}}
twitter={{ cardType: 'summary_large_image' }}
/>
<BaseLayout>
<Page content={content} />
@@ -1,15 +1,17 @@
---
import { AstroSeo } from '@astrolib/seo';
import { MagicWandIcon, StarFilledIcon } from '@radix-ui/react-icons';
import { Flex, Heading, Link } from '@radix-ui/themes';
import { compareVersions } from 'compare-versions';
import { PageWrapper } from '../../../../components/PageWrapper';
import { getStrings } from '../../../../core/i18n/getStrings';
import BaseLayout from '../../../../layouts/BaseLayout.astro';
export { getStaticPaths } from '../../index.astro';
// this can't be used in template literal :(
const ROOT = '../../../../../../../docs/changelogs/';
const strings = getStrings(Astro.params.locale);
const versions = Object.keys(
import.meta.glob('../../../../../../../docs/changelogs/*.md', {
eager: true,
@@ -20,7 +22,18 @@ const versions = Object.keys(
.reverse();
---
<title>BlitzKit Patches</title>
<AstroSeo
title={strings.website.docs.patches.name}
description={strings.website.docs.patches.seo_description}
canonical={Astro.url.href}
openGraph={{
url: Astro.url.href,
title: strings.website.docs.patches.name,
description: strings.website.docs.patches.seo_description,
site_name: 'BlitzKit',
}}
twitter={{ cardType: 'summary_large_image' }}
/>
<BaseLayout>
<PageWrapper align="center">
@@ -1,10 +1,12 @@
---
import { AstroSeo } from '@astrolib/seo';
import { assertSecret } from '@blitzkit/core';
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '@blitzkit/i18n';
import { literals } from '@blitzkit/i18n/src/literals';
import { Box, Flex, Heading, Link } from '@radix-ui/themes';
import type { GetStaticPaths } from 'astro';
import { AesonPlug } from '../../components/AesonPlug';
import { DiscordPlug } from '../../components/Discord';
import { HomeLocaleSwitcher } from '../../components/Home/HomeLocaleSwitcher';
import HomeHero from '../../components/HomeHero.astro';
import { Plugs } from '../../components/Plugs';
@@ -39,13 +41,37 @@ export const getStaticPaths = (() => {
}) satisfies GetStaticPaths;
---
<title>BlitzKit</title>
<meta name="description" content={strings.website.home.seo_description} />
<AstroSeo
title="BlitzKit"
description={strings.website.home.seo_description}
canonical={Astro.url.href}
openGraph={{
url: Astro.url.href,
title: 'BlitzKit',
description: strings.website.home.seo_description,
site_name: 'BlitzKit',
}}
twitter={{ cardType: 'summary_large_image' }}
/>
<BaseLayout>
<HomeHero />
<HomeLocaleSwitcher {locale} client:load />
<Flex justify="center" py="6" px="4" justify="center">
<Flex
wrap="wrap"
justify="between"
maxWidth="64rem"
gap="6"
align="center"
flexGrow="1"
>
<HomeLocaleSwitcher {locale} client:load />
<DiscordPlug {locale} client:only="react">
<DiscordPlug {locale} skeleton slot="fallback" />
</DiscordPlug>
</Flex>
</Flex>
{
locale !== DEFAULT_LOCALE && (
@@ -75,7 +101,7 @@ export const getStaticPaths = (() => {
}
<Flex p="4" justify="center" gap="4" mt="3">
<Flex maxWidth="64rem" flexGrow="1" gap="4" direction="column">
<Flex maxWidth="80rem" flexGrow="1" gap="4" direction="column">
<ToolCard tool={tanksTool} />
<!-- {
@@ -119,16 +145,16 @@ export const getStaticPaths = (() => {
<ToolCard
tool={{
button: { color: 'blue' },
id: 'blitz_analysis',
image: 'j76YXGl',
id: 'blitz-analysis',
stringsId: 'blitz_analysis',
href: 'https://blitzanalysiz.com/',
}}
/>
<ToolCard
tool={{
button: { color: 'purple', highContrast: true },
image: 'zYrrYbR',
id: 'blitz_insider',
id: 'blitz-insider',
stringsId: 'blitz_insider',
href: 'https://discord.gg/UjWf9eGgtR',
}}
/>
+11 -15
View File
@@ -1,23 +1,19 @@
import { assertSecret } from '@blitzkit/core';
import type { APIContext } from 'astro';
export async function GET({}: APIContext) {
export async function GET({ request }: APIContext) {
const allowRobots = assertSecret(import.meta.env.ALLOW_ROBOTS) === 'true';
const site = assertSecret(import.meta.env.SITE);
const flags: Record<string, string | null> = {
'User-agent': '*',
[allowRobots ? 'Allow' : 'Disallow']: '/',
space: null,
Sitemap: `${new URL(request.url).origin}/sitemap-index.xml`,
};
return new Response(
`
User-agent: *
Allow: /
${
allowRobots
? `
Host: ${site}
Sitemap: ${site}/sitemap-index.xml
`
: ''
}
`.trim(),
Object.entries(flags)
.map(([key, value]) => (value === null ? '' : `${key}: ${value}`))
.join('\n'),
);
}
+8 -1
View File
@@ -1,4 +1,9 @@
import type { GunDefinition, TankClass, TankType } from '@blitzkit/core';
import type {
GunDefinition,
ShellType,
TankClass,
TankType,
} from '@blitzkit/core';
import { map } from 'nanostores';
export type CaseType<T> = T extends {
@@ -16,6 +21,7 @@ export interface TankFilters {
search?: string;
searching: boolean;
gunType: CaseType<GunDefinition>[];
shells: [ShellType | null, ShellType | null, ShellType | null];
}
export const initialTankFilters: TankFilters = {
@@ -27,6 +33,7 @@ export const initialTankFilters: TankFilters = {
search: undefined,
searching: false,
gunType: [],
shells: [null, null, null],
};
export const $tankFilters = map<TankFilters>(initialTankFilters);