feat: move to locale

This commit is contained in:
tresabhi committed 2024-12-27 12:38:38 -06:00
1 parent 60ab845326
commit 1843eb3bcf
44 files changed
+234 -223

No files matched your search

@@ -0,0 +1,61 @@
import { useEffect } from 'react';
import { App } from '../../../../stores/app';
export const AUTH_PROVIDERS = ['wargaming'] as const;
type AuthProvider = (typeof AUTH_PROVIDERS)[number];
export const AUTH_PROVIDER_NAMES: Record<AuthProvider, string> = {
wargaming: 'Wargaming',
};
interface AuthorizeProps {
provider: AuthProvider;
}
export function Authorize(props: AuthorizeProps) {
return (
<App.Provider>
<Content {...props} />
</App.Provider>
);
}
function Content({ provider }: AuthorizeProps) {
const mutateApp = App.useMutation();
useEffect(() => {
(async () => {
const searchParams = new URLSearchParams(window.location.search);
switch (provider) {
case 'wargaming': {
if (
!searchParams.has('expires_at') ||
!searchParams.has('access_token') ||
!searchParams.has('account_id')
) {
break;
}
mutateApp((draft) => {
draft.logins.wargaming = {
expires: Number(searchParams.get('expires_at')) * 1000,
token: searchParams.get('access_token')!,
id: Number(searchParams.get('account_id'))!,
};
});
break;
}
default:
throw new Error(`Unknown provider: ${provider}`);
}
window.location.href = searchParams.get('return') ?? '/';
})();
}, []);
return null;
}
@@ -0,0 +1,32 @@
---
import { Flex, Heading, Text } from '@radix-ui/themes';
import type { GetStaticPaths } from 'astro';
import { PageWrapper } from '../../../../components/PageWrapper';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { AUTH_PROVIDERS, AUTH_PROVIDER_NAMES, Authorize } from './_index';
export const getStaticPaths = (() => {
return AUTH_PROVIDERS.map((provider) => ({ params: { provider } }));
}) satisfies GetStaticPaths;
const { provider } = Astro.params;
---
<BaseLayout>
<PageWrapper>
<Flex
style={{ flex: 1 }}
align="center"
justify="center"
direction="column"
gap="2"
>
<Heading align="center"
>Authorizing you with {AUTH_PROVIDER_NAMES[provider]}...</Heading
>
<Text color="gray" align="center">You'll be on your way shortly.</Text>
</Flex>
</PageWrapper>
<Authorize client:only="react" {provider} />
</BaseLayout>
@@ -0,0 +1,14 @@
import { Markdown } from '../../../../../components/Markdown';
import { PageWrapper } from '../../../../../components/PageWrapper';
interface PageProps {
content: string;
}
export function Page({ content }: PageProps) {
return (
<PageWrapper>
<Markdown>{content}</Markdown>
</PageWrapper>
);
}
@@ -0,0 +1,38 @@
---
import type { GetStaticPaths } from 'astro';
import { readFile } from 'fs/promises';
import BaseLayout from '../../../../layouts/BaseLayout.astro';
import { Page } from './_index';
export const getStaticPaths = (async () => {
const ROOT = '../../../../../../../docs/';
const files = import.meta.glob('../../../../../../../docs/**/*.md', {
eager: true,
});
return await Promise.all(
Object.keys(files).map(async (file) => {
const couple = file.replace(ROOT, '').split('/');
couple[1] = couple[1].replace('.md', '');
const content = await readFile(
`../../docs/${couple[0]}/${couple[1]}.md`,
).then((buffer) => buffer.toString());
return {
params: { directory: couple[0], document: couple[1] },
props: { content },
};
}),
);
}) satisfies GetStaticPaths;
const { content } = Astro.props;
const title = content.split('\n')[0].replace('# ', '');
---
<title>{title}</title>
<BaseLayout>
<Page content={content} />
</BaseLayout>
@@ -0,0 +1,50 @@
---
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 BaseLayout from '../../../layouts/BaseLayout.astro';
// this can't be used in template literal :(
const ROOT = '../../../../../../docs/changelogs/';
const versions = Object.keys(
import.meta.glob(`../../../../../../docs/changelogs/*.md`, {
eager: true,
}),
)
.map((file) => file.replace(ROOT, '').replace('.md', ''))
.sort(compareVersions)
.reverse();
---
<title>BlitzKit Patches</title>
<BaseLayout>
<PageWrapper align="center">
<Heading mt="6" mb="4">BlitzKit patches</Heading>
<Flex direction="column" gap="2" align="center">
{
versions.map((version) => {
const split = version.split('.').map(Number);
const isMajor = split[2] === 0;
return (
<Link
size="5"
href={`/docs/changelogs/${version}`}
mb={isMajor ? '6' : '0'}
>
<Flex align="center" gap="2">
{!isMajor && <MagicWandIcon />}
{isMajor && <StarFilledIcon />}
{version}
</Flex>
</Link>
);
})
}
</Flex>
</PageWrapper>
</BaseLayout>
@@ -0,0 +1,113 @@
---
import { assertSecret, SUPPORTED_LOCALES } from '@blitzkit/core';
import { Box, Flex, Heading } from '@radix-ui/themes';
import type { GetStaticPaths } from 'astro';
import { AesonPlug } from '../../components/AesonPlug';
import HomeHero from '../../components/HomeHero.astro';
import { HomePageHotTanks } from '../../components/HomePageHotTanks';
import { Plugs } from '../../components/Plugs';
import ToolCard from '../../components/ToolCard.astro';
import {
compareTool,
discordTool,
embedTool,
galleryTool,
moreTool,
performanceTool,
sessionTool,
tankopediaTool,
tierListTool,
} from '../../constants/tools';
import BaseLayout from '../../layouts/BaseLayout.astro';
const branch = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
export const getStaticPaths = (() => {
return [
...SUPPORTED_LOCALES.map((locale) => ({ params: { locale } })),
{ params: { locale: undefined } },
];
}) satisfies GetStaticPaths;
---
<BaseLayout>
<HomeHero />
<Plugs client:idle />
{
branch === 'preview' && (
<Flex justify="center" my="8" px="4">
<AesonPlug />
</Flex>
)
}
<Flex p="4" justify="center" gap="4" mt="3">
<Flex maxWidth="64rem" flexGrow="1" gap="4" direction="column">
<ToolCard tool={tankopediaTool} />
<HomePageHotTanks />
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
{/* <ToolCard tool={playerStatsTool} /> */}
<ToolCard tool={compareTool} />
<ToolCard tool={performanceTool} />
</Flex>
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<!-- <ToolCard tool={chartsTool} /> -->
<ToolCard tool={galleryTool} />
<ToolCard tool={sessionTool} />
</Flex>
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<ToolCard tool={discordTool} />
<ToolCard tool={tierListTool} />
</Flex>
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<ToolCard tool={embedTool} />
<ToolCard tool={moreTool} />
</Flex>
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<Box flexGrow="1" />
</Flex>
<Heading align="center" mt="6">Other cool projects</Heading>
<Flex gap="4" mb="6" direction={{ initial: 'column', sm: 'row' }}>
<ToolCard
tool={{
button: {
text: 'Analyze',
color: 'blue',
},
description: 'Rich historic average stats',
id: 'blitz-analysis',
image: 'j76YXGl',
title: 'BlitzAnalysis[]',
href: 'https://blitzanalysiz.com/',
}}
/>
<ToolCard
tool={{
button: {
color: 'purple',
text: 'Install',
highContrast: true,
},
image: 'zYrrYbR',
description: 'In-game session stats',
id: 'blitz-insider',
title: 'Blitz Insider',
href: 'https://discord.gg/UjWf9eGgtR',
}}
/>
</Flex>
</Flex>
</Flex>
<div style={{ flex: 1 }}></div>
</BaseLayout>
@@ -0,0 +1,36 @@
import { Flex, Heading, Switch, Text } from '@radix-ui/themes';
import { PageWrapper } from '../../../components/PageWrapper';
import { App } from '../../../stores/app';
export function Page() {
return (
<App.Provider>
<Content />
</App.Provider>
);
}
function Content() {
const developerMode = App.useDeferred((state) => state.developerMode, false);
const appStore = App.useStore();
return (
<PageWrapper justify="center" align="center">
<Flex direction="column" gap="4">
<Flex gap="3" direction="column">
<Heading size="5">Advanced</Heading>
<Flex align="center" gap="2">
<Text>Developer mode</Text>
<Switch
checked={developerMode}
onCheckedChange={(checked) =>
appStore.setState({ developerMode: checked })
}
/>
</Flex>
</Flex>
</Flex>
</PageWrapper>
);
}
@@ -0,0 +1,8 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { Page } from './_index';
---
<BaseLayout>
<Page client:load />
</BaseLayout>
@@ -0,0 +1,29 @@
import { Flex } from '@radix-ui/themes';
import { ChartsControl } from '../../../../components/Charts/ChartsControl';
import { PlayersInPeriod } from '../../../../components/Charts/PlayersInPeriod';
import { PageWrapper } from '../../../../components/PageWrapper';
import { FilterControl } from '../../../../components/TankSearch/components/FilterControl';
import { Charts } from '../../../../stores/charts';
export function Page() {
return (
<Charts.Provider>
<Content />
</Charts.Provider>
);
}
function Content() {
return (
<PageWrapper maxWidth="unset">
<FilterControl />
<ChartsControl />
<Flex justify="center">
<Flex direction="column" maxWidth="45rem" flexGrow="1" py="6" gap="9">
<PlayersInPeriod />
</Flex>
</Flex>
</PageWrapper>
);
}
@@ -0,0 +1,9 @@
---
import { chartsTool } from '../../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro';
import { Page } from './_index';
---
<ToolLayout tool={chartsTool}>
<Page client:load />
</ToolLayout>
@@ -0,0 +1,170 @@
import { createDefaultSkills } from '@blitzkit/core';
import { PlusIcon } from '@radix-ui/react-icons';
import { Flex, Heading, Text } from '@radix-ui/themes';
import { useEffect, useMemo, useState } from 'react';
import { CompareTable } from '../../../../components/Compare/CompareTable';
import { Controls } from '../../../../components/Compare/Controls';
import { DamageWarning } from '../../../../components/DamageWarning';
import { PageWrapper } from '../../../../components/PageWrapper';
import { awaitableEquipmentDefinitions } from '../../../../core/awaitables/equipmentDefinitions';
import { awaitableModelDefinitions } from '../../../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../../../core/awaitables/provisionDefinitions';
import { awaitableSkillDefinitions } from '../../../../core/awaitables/skillDefinitions';
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
import { tankCharacteristics } from '../../../../core/blitzkit/tankCharacteristics';
import { tankToCompareMember } from '../../../../core/blitzkit/tankToCompareMember';
import { App } from '../../../../stores/app';
import { CompareEphemeral } from '../../../../stores/compareEphemeral';
import { ComparePersistent } from '../../../../stores/comparePersistent';
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
const [
modelDefinitions,
equipmentDefinitions,
provisionDefinitions,
skillDefinitions,
tankDefinitions,
] = await Promise.all([
awaitableModelDefinitions,
awaitableEquipmentDefinitions,
awaitableProvisionDefinitions,
awaitableSkillDefinitions,
awaitableTankDefinitions,
]);
export function Page() {
return (
<App.Provider>
<TankopediaPersistent.Provider>
<ComparePersistent.Provider>
<CompareEphemeral.Provider
data={createDefaultSkills(skillDefinitions)}
>
<Content />
</CompareEphemeral.Provider>
</ComparePersistent.Provider>
</TankopediaPersistent.Provider>
</App.Provider>
);
}
function Content() {
const members = CompareEphemeral.use((state) => state.members);
const [addTankDialogOpen, setAddTankDialogOpen] = useState(false);
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
const stats = useMemo<ReturnType<typeof tankCharacteristics>[]>(
() =>
members.map((thisMember) =>
tankCharacteristics(
{
...thisMember,
crewSkills,
stockEngine: thisMember.tank.engines[0],
stockGun: thisMember.tank.turrets[0].guns[0],
stockTrack: thisMember.tank.tracks[0],
stockTurret: thisMember.tank.turrets[0],
applyReactiveArmor: members.some(
(member) =>
member.key !== thisMember.key &&
member.consumables.includes(33),
),
applyDynamicArmor: members.some(
(member) =>
member.key !== thisMember.key &&
member.consumables.includes(73),
),
applySpallLiner: members.some(
(member) =>
member.key !== thisMember.key &&
member.provisions.includes(101),
),
},
{
equipmentDefinitions: equipmentDefinitions,
provisionDefinitions: provisionDefinitions,
tankModelDefinition: modelDefinitions.models[thisMember.tank.id],
},
),
),
[members, crewSkills],
);
const mutateCompareEphemeral = CompareEphemeral.useMutation();
useEffect(() => {
const search = new URLSearchParams(window.location.search);
const tanksQuery = search.get('tanks');
if (tanksQuery === null) return;
const tanks = tanksQuery.split(',').map(Number);
mutateCompareEphemeral((draft) => {
draft.members = [];
tanks.forEach((id) => {
const tank = tankDefinitions.tanks[id];
if (tank === undefined) return;
draft.members.push(tankToCompareMember(tank, provisionDefinitions));
});
});
}, []);
useEffect(() => {
const search = new URLSearchParams(window.location.search);
search.set('tanks', members.map((member) => member.tank.id).join(','));
window.history.replaceState(
{},
'',
`${window.location.pathname}?${search.toString()}`,
);
}, [members]);
return (
<PageWrapper color="crimson" maxWidth="100%">
<Flex justify="center" gap="4" align="center" direction="column">
<DamageWarning />
<Controls
addTankDialogOpen={addTankDialogOpen}
onAddTankDialogOpenChange={setAddTankDialogOpen}
/>
</Flex>
{members.length > 0 && (
<Flex justify="center" flexGrow="1" position="relative">
<CompareTable stats={stats} />
{/* <IconButton
style={{
position: 'absolute',
right: 0,
top: 73,
transform: 'translate(50%, -50%)',
zIndex: 1,
}}
onClick={() => setAddTankDialogOpen(true)}
>
<PlusIcon />
</IconButton> */}
</Flex>
)}
{members.length === 0 && (
<Flex
align="center"
justify="center"
direction="column"
style={{ flex: 1 }}
>
<Heading color="gray">No tanks selected</Heading>
<Text color="gray">
Press the <PlusIcon /> Add button to get started
</Text>
</Flex>
)}
</PageWrapper>
);
}
@@ -0,0 +1,12 @@
---
import { compareTool } from '../../../../constants/tools';
import ToolOpenGraph from '../../../components/ToolOpenGraph.astro';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { Page } from './_index';
---
<ToolOpenGraph tool={compareTool} />
<BaseLayout noFooter>
<Page client:load />
</BaseLayout>
@@ -0,0 +1,275 @@
import { imgur } from '@blitzkit/core';
import {
CodeIcon,
HeightIcon,
ImageIcon,
ResetIcon,
TimerIcon,
WidthIcon,
} from '@radix-ui/react-icons';
import { Box, Button, Flex, Heading, ScrollArea, Text } from '@radix-ui/themes';
import { capitalize, startCase } from 'lodash-es';
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { CopyButton } from '../../../../../components/CopyButton';
import { Boolean } from '../../../../../components/Embeds/Boolean';
import { Color } from '../../../../../components/Embeds/Color';
import { Enum } from '../../../../../components/Embeds/Enum';
import { GenerateURL } from '../../../../../components/Embeds/GenerateURL';
import { Import } from '../../../../../components/Embeds/Import';
import { PreviewWrapper } from '../../../../../components/Embeds/PreviewWrapper';
import { Radius } from '../../../../../components/Embeds/Radius';
import { RichText } from '../../../../../components/Embeds/RichText';
import { Size } from '../../../../../components/Embeds/Size';
import { SizeWithout0 } from '../../../../../components/Embeds/SizeWithout0';
import { Slider } from '../../../../../components/Embeds/Slider';
import { TextController } from '../../../../../components/Embeds/TextController';
import { PageWrapper } from '../../../../../components/PageWrapper';
import {
embedConfigurations,
extractEmbedConfigDefaults,
} from '../../../../../constants/embeds';
import { NAVBAR_HEIGHT } from '../../../../../constants/navbar';
import { App } from '../../../../../stores/app';
import { EmbedState, type EmbedConfig } from '../../../../../stores/embedState';
import { EmbedItemType } from '../../../../../stores/embedState/constants';
export interface EmbedPreviewControllerProps {
configKey: string;
}
interface PageProps {
embed: keyof typeof embedConfigurations;
}
export function Page({ embed }: PageProps) {
const config = embedConfigurations[embed];
return (
<App.Provider>
<EmbedState.Provider data={extractEmbedConfigDefaults(config)}>
<Content embed={embed} />
</EmbedState.Provider>
</App.Provider>
);
}
function Content({ embed }: PageProps) {
const embedStateStore = EmbedState.useStore();
const config = embedConfigurations[embed] as EmbedConfig;
const [backgroundImage, setBackgroundImage] = useState(
imgur('SO13zur', { format: 'jpeg' }),
);
const fileInput = useRef<HTMLInputElement>();
useEffect(() => {
fileInput.current = document.createElement('input');
fileInput.current.type = 'file';
fileInput.current.style.display = 'none';
fileInput.current.accept = 'image/*';
fileInput.current.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
setBackgroundImage(URL.createObjectURL(file));
};
}, []);
return (
<PageWrapper padding="0" maxWidth="104rem" color="red">
<Flex>
<ScrollArea
scrollbars="vertical"
style={{ height: '100%', maxWidth: 320 }}
>
<Flex direction="column" gap="2" p="4">
<Import />
<Heading>Export</Heading>
<Text size="2" color="gray" mb="2">
Copy all properties for you streaming software
</Text>
<Flex mb="6" gap="2" wrap="wrap">
<GenerateURL embed={embed} />
<CopyButton
variant="outline"
copy={() =>
'body { margin: 0; background-color: transparent; overflow: hidden; }'
}
>
<CodeIcon />
Copy CSS
</CopyButton>
<CopyButton
variant="outline"
copy={() => `${embedStateStore.getState().width}`}
>
<WidthIcon />
Copy width
</CopyButton>
<CopyButton
variant="outline"
copy={() => `${embedStateStore.getState().height}`}
>
<HeightIcon />
Copy height
</CopyButton>
<CopyButton variant="outline" copy={() => '1'}>
<TimerIcon />
Copy framerate
</CopyButton>
</Flex>
<Flex justify="between" align="center">
<Heading>Customize</Heading>
<Button
color="red"
variant="ghost"
onClick={() => {
embedStateStore.setState(
embedStateStore.getInitialState(),
true,
);
}}
>
<ResetIcon /> Reset
</Button>
</Flex>
<Text size="2" color="gray" mb="2">
Bells and whistles of your embed
</Text>
{Object.keys(config).map((configKey) => {
const item = config[configKey];
let control: ReactNode;
let oneLiner = false;
switch (item.type) {
case EmbedItemType.Boolean: {
control = <Boolean configKey={configKey} />;
oneLiner = true;
break;
}
case EmbedItemType.Color: {
control = <Color configKey={configKey} />;
break;
}
case EmbedItemType.Radius: {
control = <Radius configKey={configKey} />;
oneLiner = true;
break;
}
case EmbedItemType.Size: {
control = <Size configKey={configKey} />;
oneLiner = true;
break;
}
case EmbedItemType.SizeWithout0: {
control = <SizeWithout0 configKey={configKey} />;
oneLiner = true;
break;
}
case EmbedItemType.Slider: {
control = <Slider configKey={configKey} config={item} />;
oneLiner = true;
break;
}
case EmbedItemType.String: {
control = <TextController configKey={configKey} />;
break;
}
case EmbedItemType.RichText: {
control = <RichText configKey={configKey} />;
break;
}
case EmbedItemType.Enum: {
control = <Enum configKey={configKey} config={item} />;
oneLiner = true;
break;
}
}
return (
<Flex
key={configKey}
direction={oneLiner ? undefined : 'column'}
gap={oneLiner ? '2' : '1'}
justify={oneLiner ? 'between' : undefined}
mb={oneLiner ? '2' : '4'}
pb={item.pad ? '6' : undefined}
>
<Text>{capitalize(startCase(configKey as string))}</Text>
<Flex
gap="2"
align="center"
flexGrow="1"
justify={oneLiner ? 'end' : undefined}
>
{control}
</Flex>
</Flex>
);
})}
</Flex>
</ScrollArea>
<Box
p="4"
flexGrow="1"
position="sticky"
top={`${NAVBAR_HEIGHT}px`}
height={`calc(100vh - ${NAVBAR_HEIGHT}px)`}
>
<Flex
direction="column"
height="100%"
style={{
borderRadius: 'var(--radius-3)',
overflow: 'hidden',
boxShadow: 'var(--shadow-3)',
}}
>
<Flex
justify="between"
align="center"
p="2"
pl="3"
style={{
backgroundColor: 'var(--color-panel-solid)',
}}
>
<Text color="gray">Example preview</Text>
<Button
variant="outline"
color="gray"
onClick={() => fileInput.current?.click()}
>
<ImageIcon /> Upload test background
</Button>
</Flex>
<Box
flexGrow="1"
style={{
background: `url(${backgroundImage}) center / cover no-repeat`,
}}
position="relative"
overflow="hidden"
>
<PreviewWrapper name={embed} />
</Box>
</Flex>
</Box>
</Flex>
</PageWrapper>
);
}
@@ -0,0 +1,54 @@
import { Box } from '@radix-ui/themes';
import { useMemo } from 'react';
import { parse } from 'urlon';
import { BlitzKitTheme } from '../../../../../../components/BlitzKitTheme';
import {
embedConfigurations,
embedRenderers,
extractEmbedConfigDefaults,
} from '../../../../../../constants/embeds';
import {
EmbedState,
type EmbedConfig,
type ExtractEmbedConfigTypes,
} from '../../../../../../stores/embedState';
import { useEmbedStateCurry } from '../../../../../../stores/embedState/utilities';
interface PageProps {
embed: keyof typeof embedConfigurations;
}
export function Page({ embed }: PageProps) {
const state = useMemo(() => {
const params = new URLSearchParams(window.location.search);
const stateRaw = params.get('state');
const partialState = stateRaw === null ? {} : parse(stateRaw);
const state = {
...extractEmbedConfigDefaults(embedConfigurations[embed]),
...partialState,
} as ExtractEmbedConfigTypes<EmbedConfig>;
return state;
}, [embed]);
return (
<EmbedState.Provider data={state}>
<Content embed={embed} />
</EmbedState.Provider>
);
}
function Content({ embed }: PageProps) {
const Renderer = embedRenderers[embed];
const { useEmbedState } = useEmbedStateCurry();
const width = useEmbedState('width');
const height = useEmbedState('height');
return (
<BlitzKitTheme style={{ background: 'transparent' }}>
<Box width={`${width}px`} height={`${height}px`} overflow="hidden">
<Renderer />
</Box>
</BlitzKitTheme>
);
}
@@ -0,0 +1,13 @@
---
import { capitalize } from 'lodash-es';
import EmbedLayout from '../../../../../layouts/EmbedLayout.astro';
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>
@@ -0,0 +1,38 @@
---
import type { GetStaticPaths } from 'astro';
import { capitalize } from 'lodash-es';
import { embedConfigurations } from '../../../../../constants/embeds';
import BaseLayout from '../../../../layouts/BaseLayout.astro';
import { Page } from './_index';
export const getStaticPaths = (() => {
return Object.keys(embedConfigurations).map((embed) => ({
params: { embed: embed as keyof typeof embedConfigurations },
}));
}) satisfies GetStaticPaths;
---
<title>BlitzKit - {capitalize(Astro.params.embed)} embed</title>
<style is:global>
@keyframes fade-out-border {
from {
outline: 1rem solid #ff0000ff;
}
to {
outline: 0.75rem solid #ff000000;
}
}
.preview-container-animated {
animation: fade-out-border 0.5s linear;
}
.preview-container-animated:hover {
outline: 1rem solid #ff000080;
}
</style>
<BaseLayout>
<Page client:load embed={Astro.params.embed} />
</BaseLayout>
@@ -0,0 +1,110 @@
import { imgur, ImgurSize } from '@blitzkit/core';
import { Box, Flex, Link, Text } from '@radix-ui/themes';
import { capitalize } from 'lodash-es';
import { PreviewWrapper } from '../../../../components/Embeds/PreviewWrapper';
import { PageWrapper } from '../../../../components/PageWrapper';
import {
embedConfigurations,
extractEmbedConfigDefaults,
} from '../../../../constants/embeds';
import { Var } from '../../../../core/radix/var';
import { App } from '../../../../stores/app';
import { EmbedState } from '../../../../stores/embedState';
export function Page() {
return (
<App.Provider>
<Content />
</App.Provider>
);
}
function Content() {
// const wargaming = App.useDeferred((state) => state.logins.wargaming, {
// token: '',
// id: 0,
// expires: 0,
// });
return (
<PageWrapper color="red">
{/* {!wargaming && (
<Flex direction="column" align="center" gap="2">
<Callout.Root>
<Callout.Icon>
<ExclamationTriangleIcon />
</Callout.Icon>
<Callout.Text>
<Flex align="center">You must log in to generate embeds.</Flex>
</Callout.Text>
</Callout.Root>
<WargamingLoginButton>Log in with Wargaming</WargamingLoginButton>
</Flex>
)} */}
<Text align="center" mt="3">
More embeds coming soon.{' '}
<Link
underline="always"
href="https://discord.gg/nDt7AjGJQH"
target="_blank"
>
Suggest ideas
</Link>
.
</Text>
<Flex justify="center">
{Object.entries(embedConfigurations).map(([embed, config]) => (
<EmbedState.Provider
key={embed}
data={extractEmbedConfigDefaults(config)}
>
<Link
href={`/tools/embed/${embed}`}
color="gray"
highContrast
style={{
width: 'fit-content',
height: 'fit-content',
}}
underline="hover"
>
<Flex
direction="column"
overflow="hidden"
style={{
borderRadius: Var('radius-4'),
backgroundColor: Var('color-panel-solid'),
boxShadow: Var('shadow-3'),
}}
>
<Flex justify="center" p="2">
<Text>{capitalize(embed)}</Text>
</Flex>
<Box
width="20rem"
height="20rem"
overflow="hidden"
position="relative"
style={{
backgroundImage: `url(${imgur('SO13zur', { format: 'jpeg', size: ImgurSize.SmallThumbnail })})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<PreviewWrapper
naked
name={embed as keyof typeof embedConfigurations}
/>
</Box>
</Flex>
</Link>
</EmbedState.Provider>
))}
</Flex>
</PageWrapper>
);
}
@@ -0,0 +1,9 @@
---
import { embedTool } from '../../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro';
import { Page } from './_index';
---
<ToolLayout tool={embedTool}>
<Page />
</ToolLayout>
@@ -0,0 +1,31 @@
import { CalendarIcon } from '@radix-ui/react-icons';
import { Callout, Flex } from '@radix-ui/themes';
import { GalleryList, type Avatar } from '../../../../components/Gallery/List';
import { GallerySearch } from '../../../../components/Gallery/Search';
import { PageWrapper } from '../../../../components/PageWrapper';
import { GalleryEphemeral } from '../../../../stores/galleryEphemeral';
interface PageProps {
avatars: Avatar[];
lastUpdated: string;
}
export function Page({ avatars, lastUpdated }: PageProps) {
return (
<PageWrapper maxWidth="80rem" color="gold">
<Flex justify="center">
<Callout.Root size="1">
<Callout.Icon>
<CalendarIcon />
</Callout.Icon>
<Callout.Text>Last updated on {lastUpdated}</Callout.Text>
</Callout.Root>
</Flex>
<GalleryEphemeral.Provider>
<GallerySearch />
<GalleryList avatars={avatars} />
</GalleryEphemeral.Provider>
</PageWrapper>
);
}
@@ -0,0 +1,26 @@
---
import type { Avatar } from '../../../../components/Gallery/List';
import { galleryTool } from '../../../../constants/tools';
import { fetchGlossary } from '../../../../core/blitz/fetchGlossary';
import ToolLayout from '../../../layouts/ToolLayout.astro';
import { Page } from './_index';
const glossary = await fetchGlossary();
const avatars = Object.entries(glossary)
.filter(
([key, value]) =>
key.startsWith('avatar') &&
!key.endsWith('_part') &&
value.image_url !== null,
)
.map(
([key, value]) =>
({ id: key.replace('avatar_', ''), name: value.title }) satisfies Avatar,
)
.sort((a, b) => a.name.localeCompare(b.name));
const lastUpdated = new Date().toLocaleDateString();
---
<ToolLayout tool={galleryTool}>
<Page {avatars} {lastUpdated} client:load />
</ToolLayout>
@@ -0,0 +1,30 @@
import { PageWrapper } from '../../../../components/PageWrapper';
import { PerformanceInfo } from '../../../../components/Performance/PerformanceInfo';
import { PlayerCountControl } from '../../../../components/Performance/PlayerCountControl';
import { TankTable } from '../../../../components/Performance/Table';
import { FilterControl } from '../../../../components/TankSearch/components/FilterControl';
import { TankPerformanceEphemeral } from '../../../../stores/tankPerformanceEphemeral';
import { TankPerformanceSort } from '../../../../stores/tankPerformanceSort';
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps';
export function Page({ skeleton }: MaybeSkeletonComponentProps) {
return (
<TankPerformanceSort.Provider>
<TankPerformanceEphemeral.Provider>
<Content skeleton={skeleton} />
</TankPerformanceEphemeral.Provider>
</TankPerformanceSort.Provider>
);
}
function Content({ skeleton }: MaybeSkeletonComponentProps) {
return (
<PageWrapper color="jade" maxWidth="100%">
<PerformanceInfo skeleton={skeleton} />
<FilterControl />
<PlayerCountControl />
<TankTable skeleton={skeleton} />
</PageWrapper>
);
}
@@ -0,0 +1,14 @@
---
import { performanceTool } from '../../../../constants/tools';
import ToolOpenGraph from '../../../components/ToolOpenGraph.astro';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { Page } from './_index';
---
<ToolOpenGraph tool={performanceTool} />
<BaseLayout noFooter>
<Page client:only="react">
<Page skeleton slot="fallback" />
</Page>
</BaseLayout>
@@ -0,0 +1,460 @@
import {
compositeStats,
compositeStatsKeys,
deltaTankStats,
formatCompositeStat,
getAccountInfo,
getTankStats,
idToRegion,
searchPlayersAcrossRegions,
sumCompositeStats,
type AccountListWithServer,
type CompositeStatsKey,
type IndividualAccountInfo,
type IndividualTankStats,
} from '@blitzkit/core';
import strings from '@blitzkit/core/lang/en-US.json';
import {
ArrowDownIcon,
MagnifyingGlassIcon,
PlusIcon,
} from '@radix-ui/react-icons';
import {
AlertDialog,
Button,
DropdownMenu,
Flex,
Heading,
IconButton,
Select,
Table,
Text,
TextField,
} from '@radix-ui/themes';
import { debounce } from 'lodash-es';
import { useEffect, useMemo, useRef, useState } from 'react';
import { PageWrapper } from '../../../../components/PageWrapper';
import { StickyRowHeaderCell } from '../../../../components/StickyRowHeaderCell';
import { StickyTableRoot } from '../../../../components/StickyTableRoot';
import { TankRowHeaderCell } from '../../../../components/TankRowHeaderCell';
import { awaitableAverageDefinitions } from '../../../../core/awaitables/averageDefinitions';
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
import { Session, type SessionTracking } from '../../../../stores/session';
const [tankDefinitions, averageDefinitions] = await Promise.all([
awaitableTankDefinitions,
awaitableAverageDefinitions,
]);
export function Page() {
return (
<Session.Provider>
<Content />
</Session.Provider>
);
}
function Content() {
const [showSearch, setShowSearch] = useState(false);
const [searching, setSearching] = useState(false);
const [searchResults, setSearchResults] = useState<AccountListWithServer>([]);
const [showCCInaccessibilityPrompt, setShowCCInaccessibilityPrompt] =
useState(false);
const input = useRef<HTMLInputElement>(null);
const session = Session.use();
const mutateSession = Session.useMutation();
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[] | null>(
null,
);
const [accountInfo, setAccountInfo] = useState<
IndividualAccountInfo | null | undefined
>(undefined);
useEffect(() => {
(async () => {
const id = session.tracking ? session.player.id : undefined;
if (!id) return setAccountInfo(null);
setAccountInfo(
session.tracking ? await getAccountInfo(idToRegion(id), id) : null,
);
})();
}, [session.tracking && session.player.id]);
const delta = useMemo(
() =>
session.tracking && tankStatsB
? deltaTankStats(session.player.stats, tankStatsB)
.sort((a, b) => b.last_battle_time - a.last_battle_time)
.map((entry) => {
const tank = tankDefinitions.tanks[entry.tank_id];
const average = averageDefinitions.averages[tank.id];
const composite = compositeStats(
{ ...entry.all, battle_life_time: entry.battle_life_time },
average?.mu,
);
return { tank, composite };
})
: undefined,
[session.tracking && session.player, tankStatsB],
);
const total = useMemo(
() =>
delta
? sumCompositeStats(delta.map(({ composite }) => composite))
: undefined,
[delta],
);
const search = debounce(async () => {
if (!input.current) return;
setSearchResults(await searchPlayersAcrossRegions(input.current.value, 15));
setSearching(false);
}, 500);
async function track(id: number) {
const region = idToRegion(id);
const player = await getAccountInfo(region, id);
const stats = await getTankStats(region, id);
if (stats === null) {
setShowCCInaccessibilityPrompt(true);
return;
}
setShowSearch(false);
mutateSession((draft) => {
if (!input.current) return;
draft.tracking = true;
(draft as SessionTracking).player = {
id,
region,
since: Date.now(),
stats,
};
input.current.value = player.nickname;
});
}
useEffect(() => {
async function update() {
if (!session.tracking) return;
const tankStats = await getTankStats(
idToRegion(session.player.id),
session.player.id,
);
setTankStatsB(tankStats);
}
update();
const interval = setInterval(update, 5 * 1000);
return () => clearInterval(interval);
}, [session.tracking && session.player.id]);
return (
<PageWrapper color="blue">
<AlertDialog.Root
open={showCCInaccessibilityPrompt}
onOpenChange={setShowCCInaccessibilityPrompt}
>
<AlertDialog.Content>
<AlertDialog.Title>
You're attempting to track a CC account
</AlertDialog.Title>
<AlertDialog.Description>
CC (community contributor) accounts do not have public tank
statistics. We are in the process of adding embeds for CC accounts,
stay tuned.
</AlertDialog.Description>
<Flex mt="4" justify="end">
<AlertDialog.Action>
<Button variant="solid">Alright</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
{!session.tracking && (
<Flex
style={{ flex: 1 }}
align="center"
justify="center"
direction="column"
gap="3"
>
<Flex gap="2" align="center">
<Text color="gray">Look up a player to get started</Text>
<ArrowDownIcon />
</Flex>
<TextField.Root
size="3"
style={{
position: 'relative',
width: '75vw',
maxWidth: 480,
}}
ref={input}
placeholder="Search players..."
onChange={() => {
if (!input.current) return;
const sanitized = input.current.value.trim();
if (sanitized.length > 0) {
setShowSearch(true);
setSearching(true);
search();
} else {
setShowSearch(false);
}
}}
>
<TextField.Slot>
<MagnifyingGlassIcon />
</TextField.Slot>
</TextField.Root>
{showSearch && (
<Flex mt="2">
{searching && (
<Flex justify="center">
<Text color="gray">Searching...</Text>
</Flex>
)}
{!searching && searchResults.length === 0 && (
<Flex justify="center">
<Text color="gray">No players found</Text>
</Flex>
)}
{!searching && searchResults.length > 0 && (
<Flex direction="column" gap="2">
{searchResults.map((player) => (
<Button
key={player.account_id}
variant="ghost"
onClick={() => track(player.account_id)}
>
{player.nickname} (
{strings.common.regions.short[player.region]})
</Button>
))}
</Flex>
)}
</Flex>
)}
</Flex>
)}
{session.tracking && (
<Flex
mt="2"
mb="2"
direction={{ initial: 'column', sm: 'row' }}
align="center"
justify="between"
gap="2"
>
<Flex mt="2" mb="2" direction="column" gap="2">
<Heading size="5">Tracking {accountInfo?.nickname}</Heading>
<Text color="gray">
Since {new Date(session.player.since).toLocaleString()}
</Text>
</Flex>
<Flex gap="2" align="center" justify="center">
<Button
color="red"
onClick={async () => {
const stats = await getTankStats(
session.player.region,
session.player.id,
);
if (stats === null) {
setShowCCInaccessibilityPrompt(true);
return;
}
mutateSession((draft) => {
(draft as SessionTracking).player.stats = stats;
});
}}
>
Reset
</Button>
<Button
onClick={() => {
mutateSession((draft) => {
draft.tracking = false;
});
}}
>
Change
</Button>
</Flex>
</Flex>
)}
{session.tracking && delta && delta.length > 0 && (
<Flex justify="center">
<StickyTableRoot variant="surface">
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>Tank</Table.ColumnHeaderCell>
{session.columns.map((column, index) => (
<Table.ColumnHeaderCell
align="center"
width="0"
key={column}
style={{ position: 'relative' }}
>
{index === 0 && session.columns.length < 4 && (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton
size="1"
variant="surface"
style={{
position: 'absolute',
left: 0,
top: '50%',
transform: 'translate(-100%, -50%)',
zIndex: 1,
}}
>
<PlusIcon />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{compositeStatsKeys
.filter((item) => !session.columns.includes(item))
.map((key) => (
<DropdownMenu.Item
key={key}
onClick={() => {
mutateSession((draft) => {
(draft as SessionTracking).columns.unshift(
key,
);
});
}}
>
{strings.common.composite_stats[key]}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
)}
<Select.Root
value={column}
onValueChange={(value) => {
mutateSession((draft) => {
if (value === 'remove') {
(draft as SessionTracking).columns.splice(index, 1);
} else {
(draft as SessionTracking).columns[index] =
value as CompositeStatsKey;
}
});
}}
>
<Select.Trigger variant="ghost" />
<Select.Content>
{compositeStatsKeys
.filter(
(item) =>
!session.columns.includes(item) ||
item === column,
)
.map((key) => (
<Select.Item key={key} value={key}>
{strings.common.composite_stats[key]}
</Select.Item>
))}
<Select.Separator />
<Select.Item value="remove">Remove</Select.Item>
</Select.Content>
</Select.Root>
</Table.ColumnHeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row
style={{
overflow: 'hidden',
}}
>
<StickyRowHeaderCell
style={{
paddingLeft: 32,
position: 'relative',
overflowY: 'hidden',
}}
>
Total
</StickyRowHeaderCell>
{session.columns.map((column) => (
<Table.Cell align="center" key={column}>
{formatCompositeStat(total![column], column, total!)}
</Table.Cell>
))}
</Table.Row>
{delta.map(({ tank, composite }) => {
return (
<Table.Row
key={tank.id}
style={{
overflow: 'hidden',
}}
>
<TankRowHeaderCell tank={tank} />
{session.columns.map((column) => (
<Table.Cell align="center" key={column}>
{formatCompositeStat(
composite[column],
column,
composite,
)}
</Table.Cell>
))}
</Table.Row>
);
})}
</Table.Body>
</StickyTableRoot>
</Flex>
)}
{session.tracking && delta && delta.length === 0 && (
<Flex
align="center"
justify="center"
direction="column"
style={{
flex: 1,
}}
>
<Heading color="gray">No battles</Heading>
<Text color="gray">Go ahead and play a game</Text>
</Flex>
)}
</PageWrapper>
);
}
@@ -0,0 +1,9 @@
---
import { sessionTool } from '../../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro';
import { Page } from './_index';
---
<ToolLayout tool={sessionTool}>
<Page client:load />
</ToolLayout>
@@ -0,0 +1,61 @@
import { TankType } from '@blitzkit/core';
import { PageWrapper } from '../../../../../components/PageWrapper';
import { CalloutsSection } from '../../../../../components/Tankopedia/CalloutsSection';
import { CharacteristicsSection } from '../../../../../components/Tankopedia/CharacteristicsSection';
import { GameModeSection } from '../../../../../components/Tankopedia/GameModeSection';
import { HeroSection } from '../../../../../components/Tankopedia/HeroSection';
import { HistorySection } from '../../../../../components/Tankopedia/HistorySection';
import { MetaSection } from '../../../../../components/Tankopedia/MetaSection';
import { TankopediaCharts } from '../../../../../components/Tankopedia/TankopediaCharts';
import { TechTreeSection } from '../../../../../components/Tankopedia/TechTreeSection';
import { VideoSection } from '../../../../../components/Tankopedia/VideoSection';
import { awaitableModelDefinitions } from '../../../../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
import { awaitableTankDefinitions } from '../../../../../core/awaitables/tankDefinitions';
import { App } from '../../../../../stores/app';
import { Duel } from '../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
type PageProps = MaybeSkeletonComponentProps & {
id: number;
};
const [tankDefinitions, provisionDefinitions, modelDefinitions] =
await Promise.all([
awaitableTankDefinitions,
awaitableProvisionDefinitions,
awaitableModelDefinitions,
]);
export function Page({ id, skeleton }: PageProps) {
const tank = tankDefinitions.tanks[id];
const model = modelDefinitions.models[id];
return (
<TankopediaEphemeral.Provider data={model}>
<App.Provider>
<TankopediaPersistent.Provider>
<Duel.Provider
data={{ tank, provisionDefinitions: provisionDefinitions }}
>
<PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9">
<HeroSection skeleton={skeleton} />
<CalloutsSection />
<MetaSection />
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
<TechTreeSection skeleton={skeleton} />
)}
<CharacteristicsSection />
<GameModeSection />
<TankopediaCharts />
<VideoSection skeleton={skeleton} />
<HistorySection />
</PageWrapper>
</Duel.Provider>
</TankopediaPersistent.Provider>
</App.Provider>
</TankopediaEphemeral.Provider>
);
}
@@ -0,0 +1,96 @@
---
import {
asset,
createDefaultProvisions,
createDefaultSkills,
fetchEquipmentDefinitions,
fetchModelDefinitions,
fetchProvisionDefinitions,
fetchSkillDefinitions,
fetchTankDefinitions,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import strings from '@blitzkit/core/lang/en-US.json';
import type { GetStaticPaths } from 'astro';
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
import BaseLayout from '../../../../layouts/BaseLayout.astro';
import { genericDefaultEquipmentMatrix } from '../../../../../stores/duel/constants';
import { Page } from './_index';
export const getStaticPaths = (async () => {
const tankDefinitions = await fetchTankDefinitions();
return Object.values(tankDefinitions.tanks).map((tank) => ({
params: { id: tank.id },
}));
}) satisfies GetStaticPaths;
const { id } = Astro.params;
const tankDefinitions = await fetchTankDefinitions();
const skillDefinitions = await fetchSkillDefinitions();
const provisionDefinitions = await fetchProvisionDefinitions();
const equipmentDefinitions = await fetchEquipmentDefinitions();
const tankModelDefinitions = await fetchModelDefinitions();
const tank = tankDefinitions.tanks[id];
const tankModelDefinition = tankModelDefinitions.models[tank.id];
const name = tank.name_full ?? tank.name;
const title = `${name} - Tier ${TIER_ROMAN_NUMERALS[tank.tier]} ${
strings.common.nations_adjectives[
tank.nation as keyof typeof strings.common.nations_adjectives
]
} ${strings.common.tank_class_short[tank.class]} | BlitzKit Tankopedia | World of Tanks Blitz (WoTB)`;
const url = import.meta.env.SITE + Astro.url.pathname;
const icon = asset(`icons/tanks/big/${id}.webp`);
const turret = tank.turrets.at(-1)!;
const gun = turret.guns.at(-1)!;
const characteristics = tankCharacteristics(
{
tank,
applyDynamicArmor: false,
applyReactiveArmor: false,
applySpallLiner: false,
camouflage: true,
consumables: [],
crewSkills: createDefaultSkills(skillDefinitions),
engine: tank.engines.at(-1)!,
equipmentMatrix: genericDefaultEquipmentMatrix,
turret,
gun,
provisions: createDefaultProvisions(tank, gun, provisionDefinitions),
shell: gun.gun_type!.value.base.shells[0],
stockEngine: tank.engines[0],
stockGun: tank.turrets[0].guns[0],
stockTurret: tank.turrets[0],
stockTrack: tank.tracks[0],
track: tank.tracks.at(-1)!,
},
{ equipmentDefinitions, provisionDefinitions, tankModelDefinition },
);
const description = `${
name
} armor, statistics, equipment, provisions, and more. ${Math.round(
characteristics.dpm,
).toLocaleString()} DPM, ${characteristics.damage.toFixed(0)}hp alpha, ${characteristics.penetration.toFixed(
0,
)}mm penetration, ${characteristics.speedForwards.toFixed(
0,
)}kph top speed, ${characteristics.health}hp health.`;
---
<title>{title}</title>
<meta property="og:title" content={title} />
<meta name="description" content={description} />
<meta property="og:description" content={description} />
<meta property="og:image" content={icon} />
<meta property="og:url" content={url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:card" content="summary" />
<meta name="twitter:image:src" content={icon} />
<BaseLayout>
<Page id={id} client:only="react">
<Page id={id} skeleton slot="fallback" />
</Page>
</BaseLayout>
@@ -0,0 +1,16 @@
import { PageWrapper } from '../../../../components/PageWrapper';
import { TankSearch } from '../../../../components/TankSearch';
import { App } from '../../../../stores/app';
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
export function Page() {
return (
<PageWrapper color="purple" maxWidth="80rem">
<TankopediaPersistent.Provider>
<App.Provider>
<TankSearch />
</App.Provider>
</TankopediaPersistent.Provider>
</PageWrapper>
);
}
@@ -0,0 +1,11 @@
---
import { tankopediaTool } from '../../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro';
import { Page } from './_index';
---
<ToolLayout tool={tankopediaTool}>
<Page client:only="react">
<Page slot="fallback" />
</Page>
</ToolLayout>
@@ -0,0 +1,30 @@
import { PageWrapper } from '../../../../components/PageWrapper';
import { FilterControl } from '../../../../components/TankSearch/components/FilterControl';
import { TierListControls } from '../../../../components/TierList/Controls';
import { TierListTable } from '../../../../components/TierList/Table';
import { TierListTiles } from '../../../../components/TierList/Tiles';
import { URLManager } from '../../../../components/TierList/URLManager';
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
import { TierList } from '../../../../stores/tierList';
export function Page() {
return (
<TierList.Provider>
<TankopediaPersistent.Provider>
<Content />
</TankopediaPersistent.Provider>
</TierList.Provider>
);
}
function Content() {
return (
<PageWrapper color="orange" maxWidth="100rem">
<URLManager />
<TierListControls />
<TierListTable />
<FilterControl />
<TierListTiles />
</PageWrapper>
);
}
@@ -0,0 +1,9 @@
---
import { tierListTool } from '../../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro';
import { Page } from './_index';
---
<ToolLayout tool={tierListTool}>
<Page client:load />
</ToolLayout>