mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 11:39:50 +02:00
rename
This commit is contained in:
1 parent
ed92440b94
commit
f85d097c16
44 files changed
+5
-5
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import { useEffect } from "react";
|
||||
import { App } from "../../../../stores/app";
|
||||
|
||||
export const AUTH_PROVIDERS = ["wargaming"] as const;
|
||||
|
||||
export type AuthProvider = (typeof AUTH_PROVIDERS)[number];
|
||||
|
||||
export const AUTH_PROVIDER_NAMES: Record<AuthProvider, string> = {
|
||||
wargaming: "Wargaming",
|
||||
};
|
||||
|
||||
interface AuthorizeProps {
|
||||
provider: AuthProvider;
|
||||
}
|
||||
|
||||
export function Authorize({ provider }: AuthorizeProps) {
|
||||
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;
|
||||
}
|
||||
|
||||
App.mutate((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,38 @@
|
||||
---
|
||||
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 { getStaticPaths as parentGetStaticPaths } from "../../index.astro";
|
||||
import { AUTH_PROVIDER_NAMES, AUTH_PROVIDERS, Authorize } from "./_index";
|
||||
|
||||
export const getStaticPaths = (() => {
|
||||
const parentPaths = parentGetStaticPaths();
|
||||
|
||||
return AUTH_PROVIDERS.map((provider) =>
|
||||
parentPaths.map((parent) => ({ params: { ...parent.params, provider } }))
|
||||
).flat();
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { provider } = Astro.params;
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<BaseLayout {locale}>
|
||||
<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,157 @@
|
||||
import { createDefaultSkills } from "@blitzkit/core";
|
||||
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 {
|
||||
type LocaleAcceptorProps,
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
} from "../../../hooks/useLocale";
|
||||
import { CompareEphemeral } from "../../../stores/compareEphemeral";
|
||||
|
||||
const [
|
||||
modelDefinitions,
|
||||
equipmentDefinitions,
|
||||
provisionDefinitions,
|
||||
skillDefinitions,
|
||||
tankDefinitions,
|
||||
] = await Promise.all([
|
||||
awaitableModelDefinitions,
|
||||
awaitableEquipmentDefinitions,
|
||||
awaitableProvisionDefinitions,
|
||||
awaitableSkillDefinitions,
|
||||
awaitableTankDefinitions,
|
||||
]);
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
CompareEphemeral.useInitialization(createDefaultSkills(skillDefinitions));
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const members = CompareEphemeral.use((state) => state.members);
|
||||
const equalize = CompareEphemeral.use((state) => state.equalize);
|
||||
const [addTankDialogOpen, setAddTankDialogOpen] = useState(false);
|
||||
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
|
||||
const { strings } = useLocale();
|
||||
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),
|
||||
),
|
||||
assaultDistance: thisMember.assaultDistance,
|
||||
equalize,
|
||||
},
|
||||
{
|
||||
equipmentDefinitions: equipmentDefinitions,
|
||||
provisionDefinitions: provisionDefinitions,
|
||||
tankModelDefinition: modelDefinitions.models[thisMember.tank.id],
|
||||
},
|
||||
),
|
||||
),
|
||||
[members, crewSkills, equalize],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const search = new URLSearchParams(window.location.search);
|
||||
const tanksQuery = search.get("tanks");
|
||||
|
||||
if (tanksQuery === null) return;
|
||||
|
||||
const tanks = tanksQuery.split(",").map(Number);
|
||||
|
||||
CompareEphemeral.mutate((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%" px="0" pb="0">
|
||||
<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} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{members.length === 0 && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
direction="column"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Heading color="gray">
|
||||
{strings.website.tools.compare.no_tanks.title}
|
||||
</Heading>
|
||||
<Text color="gray">
|
||||
{strings.website.tools.compare.no_tanks.body}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import ToolOpenGraph from "../../../components/ToolOpenGraph.astro";
|
||||
import { tools } from "../../../constants/tools";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolOpenGraph {locale} tool={tools.compare} />
|
||||
|
||||
<BaseLayout {locale} noFooter>
|
||||
<Page {locale} client:load />
|
||||
</BaseLayout>
|
||||
+14
@@ -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 maxWidth="50rem">
|
||||
<Markdown>{content}</Markdown>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
import { AstroSeo } from "@astrolib/seo";
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { readFile } from "fs/promises";
|
||||
import BaseLayout from "../../../../../layouts/BaseLayout.astro";
|
||||
import { getStaticPaths as parentGetStaticPaths } from "../../../index.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const parentPaths = parentGetStaticPaths();
|
||||
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 parentPaths.map((parent) => ({
|
||||
params: {
|
||||
...parent.params,
|
||||
directory: couple[0],
|
||||
document: couple[1],
|
||||
},
|
||||
props: { ...parent.props, content },
|
||||
}));
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { content, locale } = Astro.props;
|
||||
|
||||
const title = content.split("\n")[0].replace("# ", "");
|
||||
---
|
||||
|
||||
<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 {locale}>
|
||||
<Page content={content} />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
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 { locale } = Astro.props;
|
||||
const strings = getStrings(locale);
|
||||
const versions = Object.keys(
|
||||
import.meta.glob("../../../../../../../docs/changelogs/*.md", {
|
||||
eager: true,
|
||||
})
|
||||
)
|
||||
.map((file) => file.replace(ROOT, "").replace(".md", ""))
|
||||
.sort(compareVersions)
|
||||
.reverse();
|
||||
---
|
||||
|
||||
<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 {locale}>
|
||||
<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,288 @@
|
||||
import {
|
||||
CodeIcon,
|
||||
HeightIcon,
|
||||
ImageIcon,
|
||||
ResetIcon,
|
||||
TimerIcon,
|
||||
WidthIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Box, Button, Flex, Heading, ScrollArea, Text } from "@radix-ui/themes";
|
||||
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 {
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../../hooks/useLocale";
|
||||
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, locale }: PageProps & LocaleAcceptorProps) {
|
||||
const config = embedConfigurations[embed];
|
||||
|
||||
EmbedState.useInitialization(extractEmbedConfigDefaults(config));
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content embed={embed} />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content({ embed }: PageProps) {
|
||||
const config = embedConfigurations[embed] as EmbedConfig;
|
||||
const [backgroundImage, setBackgroundImage] = useState(
|
||||
"/assets/images/backgrounds/embed-default.webp"
|
||||
);
|
||||
const fileInput = useRef<HTMLInputElement>(null!);
|
||||
const { strings } = useLocale();
|
||||
|
||||
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>
|
||||
{strings.website.tools.embed.configuration.export.title}
|
||||
</Heading>
|
||||
<Text size="2" color="gray" mb="2">
|
||||
{strings.website.tools.embed.configuration.export.description}
|
||||
</Text>
|
||||
|
||||
<Flex mb="6" gap="2" wrap="wrap">
|
||||
<GenerateURL embed={embed} />
|
||||
|
||||
<CopyButton
|
||||
variant="outline"
|
||||
copy={() =>
|
||||
"body { margin: 0; background-color: transparent; overflow: hidden; }"
|
||||
}
|
||||
>
|
||||
<CodeIcon />
|
||||
{strings.website.tools.embed.configuration.export.css}
|
||||
</CopyButton>
|
||||
<CopyButton
|
||||
variant="outline"
|
||||
copy={() => `${EmbedState.state.width}`}
|
||||
>
|
||||
<WidthIcon />
|
||||
{strings.website.tools.embed.configuration.export.width}
|
||||
</CopyButton>
|
||||
<CopyButton
|
||||
variant="outline"
|
||||
copy={() => `${EmbedState.state.height}`}
|
||||
>
|
||||
<HeightIcon />
|
||||
{strings.website.tools.embed.configuration.export.height}
|
||||
</CopyButton>
|
||||
<CopyButton variant="outline" copy={() => "1"}>
|
||||
<TimerIcon />
|
||||
{strings.website.tools.embed.configuration.export.framerate}
|
||||
</CopyButton>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="between" align="center">
|
||||
<Heading>
|
||||
{strings.website.tools.embed.configuration.customize.title}
|
||||
</Heading>
|
||||
<Button
|
||||
color="red"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
EmbedState.set(EmbedState.initial);
|
||||
}}
|
||||
>
|
||||
<ResetIcon />{" "}
|
||||
{strings.website.tools.embed.configuration.customize.reset}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Text size="2" color="gray" mb="2">
|
||||
{strings.website.tools.embed.configuration.customize.description}
|
||||
</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>
|
||||
{
|
||||
(
|
||||
strings.website.tools.embed.types[embed]
|
||||
.options as Record<string, string>
|
||||
)[configKey]
|
||||
}
|
||||
</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">
|
||||
{strings.website.tools.embed.preview.title}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
onClick={() => fileInput.current?.click()}
|
||||
>
|
||||
<ImageIcon /> {strings.website.tools.embed.preview.upload}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
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 {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../../../hooks/useLocale";
|
||||
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, locale }: PageProps & LocaleAcceptorProps) {
|
||||
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]);
|
||||
|
||||
EmbedState.useInitialization(state);
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content embed={embed} />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import EmbedLayout from "../../../../../layouts/EmbedLayout.astro";
|
||||
import type { getStaticPaths } from "../index.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<EmbedLayout>
|
||||
<Page {locale} client:only="react" embed={Astro.params.embed} />
|
||||
</EmbedLayout>
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
import { AstroSeo } from "@astrolib/seo";
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { capitalize } from "lodash-es";
|
||||
import { embedConfigurations } from "../../../../constants/embeds";
|
||||
import BaseLayout from "../../../../layouts/BaseLayout.astro";
|
||||
import { getStaticPaths as parentGetStaticPaths } from "../index.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export const getStaticPaths = (() => {
|
||||
const parentPaths = parentGetStaticPaths();
|
||||
|
||||
return Object.keys(embedConfigurations)
|
||||
.map((embed) =>
|
||||
parentPaths.map((parent) => ({
|
||||
params: {
|
||||
...parent.params,
|
||||
embed: embed as keyof typeof embedConfigurations,
|
||||
},
|
||||
}))
|
||||
)
|
||||
.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.props;
|
||||
---
|
||||
|
||||
<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 {
|
||||
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 {locale}>
|
||||
<Page {locale} client:load embed={Astro.params.embed} />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Box, Flex, Link, Text } from "@radix-ui/themes";
|
||||
import { PreviewWrapper } from "../../../components/Embeds/PreviewWrapper";
|
||||
import { LinkI18n } from "../../../components/LinkI18n";
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import { embedConfigurations } from "../../../constants/embeds";
|
||||
import { Var } from "../../../core/radix/var";
|
||||
import {
|
||||
type LocaleAcceptorProps,
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
} from "../../../hooks/useLocale";
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const { locale, strings } = useLocale();
|
||||
|
||||
return (
|
||||
<PageWrapper color="red">
|
||||
<Text align="center" mt="3">
|
||||
{strings.website.tools.embed.coming_soon}{" "}
|
||||
<Link
|
||||
underline="always"
|
||||
href="https://discord.gg/nDt7AjGJQH"
|
||||
target="_blank"
|
||||
>
|
||||
{strings.website.tools.embed.suggest}
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Flex justify="center">
|
||||
{Object.keys(embedConfigurations).map((embed) => (
|
||||
<LinkI18n
|
||||
locale={locale}
|
||||
href={`/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>
|
||||
{
|
||||
strings.website.tools.embed.types[
|
||||
embed as keyof typeof strings.website.tools.embed.types
|
||||
].name
|
||||
}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
width="20rem"
|
||||
height="20rem"
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(/assets/images/backgrounds/embed-default.webp)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
<PreviewWrapper
|
||||
naked
|
||||
name={embed as keyof typeof embedConfigurations}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</LinkI18n>
|
||||
))}
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import { tools } from "../../../constants/tools";
|
||||
import ToolLayout from "../../../layouts/ToolLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolLayout {locale} tool={tools.embed}>
|
||||
<Page {locale} />
|
||||
</ToolLayout>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { GalleryList } from "../../../components/Gallery/List";
|
||||
import { GallerySearch } from "../../../components/Gallery/Search";
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../hooks/useLocale";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
|
||||
export function Page({
|
||||
locale,
|
||||
...props
|
||||
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content {...props} />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<PageWrapper maxWidth="80rem" color="gold">
|
||||
<GallerySearch skeleton={skeleton} />
|
||||
<GalleryList skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import { tools } from "../../../constants/tools";
|
||||
import ToolLayout from "../../../layouts/ToolLayout.astro";
|
||||
import type { getStaticPaths } from "../index.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolLayout {locale} tool={tools.gallery}>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</ToolLayout>
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Box, Flex, Heading } from "@radix-ui/themes";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { GuessBackground } from "../../../components/GuessBackground";
|
||||
import { Guesser } from "../../../components/Guesser";
|
||||
import { GuessRenderer } from "../../../components/GuessRenderer";
|
||||
import { GuessRendererLoader } from "../../../components/GuessRendererLoader";
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import {
|
||||
type LocaleAcceptorProps,
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
} from "../../../hooks/useLocale";
|
||||
import { Duel } from "../../../stores/duel";
|
||||
import { Guess, GuessState } from "../../../stores/guess";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
|
||||
const [tankDefinitions, modelDefinitions] = await Promise.all([
|
||||
awaitableTankDefinitions,
|
||||
awaitableModelDefinitions,
|
||||
]);
|
||||
|
||||
const ids = Object.keys(tankDefinitions.tanks);
|
||||
|
||||
export function Page({
|
||||
locale,
|
||||
skeleton,
|
||||
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
|
||||
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
|
||||
Guess.useInitialization(tank);
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Container skeleton={skeleton} />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Container({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const tank = Guess.use((state) => state.tank);
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
Tankopedia.useInitialization(model);
|
||||
Duel.useInitialization({ tank, model });
|
||||
|
||||
return (
|
||||
<PageWrapper p="0" maxWidth="unset" color="cyan">
|
||||
<Content skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const { unwrap } = useLocale();
|
||||
const tank = Guess.use((state) => state.tank);
|
||||
const guessState = Guess.use((state) => state.guessState);
|
||||
const isRevealed = guessState !== GuessState.NotGuessed;
|
||||
const name = unwrap(tank.name!);
|
||||
const fontSize = `min(48vh, ${55 / name.length}vw)`;
|
||||
const transitionDuration = isRevealed ? "2s" : undefined;
|
||||
const tiers = Guess.use((state) => state.tiers);
|
||||
|
||||
useEffect(() => {
|
||||
const isTankAlreadyCompliant = tiers.includes(tank.tier);
|
||||
|
||||
if (isTankAlreadyCompliant) return;
|
||||
|
||||
const filteredIds = ids.filter((id) => {
|
||||
const tank = tankDefinitions.tanks[Number(id)];
|
||||
return tiers.includes(tank.tier);
|
||||
});
|
||||
|
||||
if (filteredIds.length === 0) return;
|
||||
|
||||
const id = Number(
|
||||
filteredIds[Math.floor(Math.random() * filteredIds.length)],
|
||||
);
|
||||
const newTank = tankDefinitions.tanks[id];
|
||||
|
||||
Guess.mutate((draft) => {
|
||||
draft.tank = newTank;
|
||||
draft.guessState = GuessState.NotGuessed;
|
||||
draft.helpingReveal = false;
|
||||
});
|
||||
}, [tiers]);
|
||||
|
||||
return (
|
||||
<Flex flexGrow="1" position="relative" overflow="hidden">
|
||||
<GuessBackground />
|
||||
|
||||
<Heading
|
||||
style={{
|
||||
transitionDuration,
|
||||
opacity: isRevealed ? 1 : 0,
|
||||
fontWeight: 900,
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
fontSize,
|
||||
userSelect: "none",
|
||||
whiteSpace: "nowrap",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Heading>
|
||||
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ transitionDuration }}
|
||||
>
|
||||
<Suspense fallback={<GuessRendererLoader />}>
|
||||
{!skeleton && <GuessRenderer />}
|
||||
</Suspense>
|
||||
</Box>
|
||||
|
||||
<Heading
|
||||
style={{
|
||||
transitionDuration,
|
||||
opacity: isRevealed ? 0.5 : 0,
|
||||
fontWeight: 900,
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
fontSize,
|
||||
userSelect: "none",
|
||||
whiteSpace: "nowrap",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Heading>
|
||||
|
||||
<Guesser />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import ToolOpenGraph from "../../../components/ToolOpenGraph.astro";
|
||||
import { tools } from "../../../constants/tools";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolOpenGraph {locale} tool={tools.guess} />
|
||||
|
||||
<BaseLayout {locale} noFooter>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
import { AstroSeo } from "@astrolib/seo";
|
||||
import { assertSecret } from "@blitzkit/core";
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { Box, Flex, Link } from "@radix-ui/themes";
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { AesonPlug } from "../../../components/AesonPlug";
|
||||
import HomeHero from "../../../components/HomeHero.astro";
|
||||
import { Plugs } from "../../../components/Plugs";
|
||||
import ToolCard from "../../../components/ToolCard.astro";
|
||||
import { tools, type Tool } from "../../../constants/tools";
|
||||
import { getStrings } from "../../../core/i18n/getStrings";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
|
||||
const MAX_TOOLS_PER_ROW = 2;
|
||||
|
||||
const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_BRANCH);
|
||||
const { locale } = Astro.props;
|
||||
const strings = getStrings(locale);
|
||||
const rows: Tool[][] = [];
|
||||
|
||||
for (const id in tools) {
|
||||
const tool = tools[id];
|
||||
|
||||
if ("disabled" in tool && tool.disabled) continue;
|
||||
|
||||
const compatibleRow = rows.find(
|
||||
(row) =>
|
||||
row.length < MAX_TOOLS_PER_ROW &&
|
||||
!("significant" in row[0] && row[0].significant),
|
||||
);
|
||||
|
||||
if (compatibleRow) {
|
||||
compatibleRow.push(tool);
|
||||
} else {
|
||||
rows.push([tool]);
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticPaths = (() => {
|
||||
return [
|
||||
...locales.supported
|
||||
.filter(({ locale }) => locale !== locales.default)
|
||||
.map(({ locale }) => ({ params: { locale }, props: { locale } })),
|
||||
{ params: { locale: undefined }, props: { locale: locales.default } },
|
||||
];
|
||||
}) satisfies GetStaticPaths;
|
||||
---
|
||||
|
||||
<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 {locale}>
|
||||
<HomeHero />
|
||||
|
||||
{
|
||||
locale !== locales.default && (
|
||||
<Flex justify="center" mb="4">
|
||||
<Link
|
||||
href="https://crowdin.com/project/blitzkrieg"
|
||||
target="_blank"
|
||||
underline="always"
|
||||
>
|
||||
{literals(strings.website.home.help_translate, {
|
||||
name: "BlitzKit",
|
||||
language:
|
||||
strings.common.locales[
|
||||
locale as keyof typeof strings.common.locales
|
||||
],
|
||||
})}
|
||||
</Link>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
<Plugs client:idle />
|
||||
|
||||
{
|
||||
PUBLIC_ASSET_BRANCH === "preview" && (
|
||||
<Flex justify="center" my="8" px="4">
|
||||
<AesonPlug />
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
<Flex p="4" justify="center" gap="4" mt="3">
|
||||
<Flex maxWidth="80rem" flexGrow="1" gap="4" direction="column">
|
||||
{
|
||||
rows.map((row) => (
|
||||
<Flex gap="4" direction={{ initial: "column", sm: "row" }}>
|
||||
{row.map((tool) => (
|
||||
<ToolCard {locale} {tool} />
|
||||
))}
|
||||
{row.length < MAX_TOOLS_PER_ROW &&
|
||||
!("significant" in row[0] && row[0].significant) && (
|
||||
<Box flexGrow="1" />
|
||||
)}
|
||||
</Flex>
|
||||
))
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<div style={{ flex: 1 }}></div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,311 @@
|
||||
import {
|
||||
asset,
|
||||
GunDefinition,
|
||||
TankDefinition,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
TurretDefinition,
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n";
|
||||
import { CaretRightIcon, CaretUpIcon, UpdateIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
Flex,
|
||||
IconButton,
|
||||
Popover,
|
||||
Skeleton,
|
||||
Text,
|
||||
type ButtonProps,
|
||||
} from "@radix-ui/themes";
|
||||
import { useRef, useState } from "react";
|
||||
import { MixerScene } from "../../../components/MixerScene";
|
||||
import { ModuleButton } from "../../../components/ModuleButtons/ModuleButton";
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import { ScreenshotButton } from "../../../components/ScreenshotButton";
|
||||
import { TankSearch } from "../../../components/TankSearch";
|
||||
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import { curateMixer } from "../../../core/blitzkit/curateMixer";
|
||||
import {
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../hooks/useLocale";
|
||||
import { Mixer } from "../../../stores/mixer";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
|
||||
const modelDefinition = await awaitableModelDefinitions.then(
|
||||
({ models }) => models[1]
|
||||
);
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
export function Page({
|
||||
locale,
|
||||
skeleton,
|
||||
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
|
||||
const initial = useRef(curateMixer());
|
||||
|
||||
Mixer.useInitialization(initial.current);
|
||||
Tankopedia.useInitialization(modelDefinition);
|
||||
|
||||
return (
|
||||
<PageWrapper color="gray" p="0" maxWidth="unset">
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content skeleton={skeleton} />
|
||||
</LocaleProvider>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
type ModuleButtonProps = Omit<ButtonProps, "onSelect"> &
|
||||
MaybeSkeletonComponentProps & {
|
||||
tank: number;
|
||||
onSelect: (
|
||||
tank: TankDefinition,
|
||||
turret?: TurretDefinition,
|
||||
gun?: GunDefinition
|
||||
) => void;
|
||||
turret?: number;
|
||||
gun?: number;
|
||||
};
|
||||
|
||||
function ModuleChooser({
|
||||
skeleton,
|
||||
tank,
|
||||
onSelect,
|
||||
turret,
|
||||
gun,
|
||||
...props
|
||||
}: ModuleButtonProps) {
|
||||
const { strings } = useLocale();
|
||||
const [open, setOpen] = useState(false);
|
||||
const tankDefinition = tankDefinitions.tanks[tank];
|
||||
const turretDefinition = tankDefinition.turrets.find((t) => t.id === turret);
|
||||
const gunDefinition = turretDefinition?.guns.find((g) => g.id === gun);
|
||||
const subSelector =
|
||||
turret !== undefined &&
|
||||
(tankDefinition.turrets.length > 1 ||
|
||||
(gun !== undefined && turretDefinition!.guns.length > 1));
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger>
|
||||
<Button
|
||||
variant="surface"
|
||||
size={{ initial: "3", md: "4" }}
|
||||
style={{
|
||||
overflow: "clip",
|
||||
borderTopRightRadius: subSelector ? 0 : undefined,
|
||||
borderBottomRightRadius: subSelector ? 0 : undefined,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{skeleton ? (
|
||||
<Skeleton width="4em" height="2em" />
|
||||
) : (
|
||||
<img
|
||||
style={{
|
||||
width: "4em",
|
||||
height: "4em",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
src={asset(`icons/tanks/big/${tank}.webp`)}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>
|
||||
{strings.website.common.tank_search.select_dialog_title}
|
||||
</Dialog.Title>
|
||||
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
onSelect?.(tank);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
{subSelector && (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton
|
||||
variant="surface"
|
||||
size={{ initial: "3", md: "4" }}
|
||||
style={{
|
||||
overflow: "clip",
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
marginLeft: -1,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<CaretUpIcon />
|
||||
</IconButton>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content>
|
||||
<Flex direction="column" gap="2">
|
||||
{tankDefinition.turrets.length > 1 && (
|
||||
<Flex gap="2">
|
||||
{tankDefinition.turrets.map((turret) => (
|
||||
<ModuleButton
|
||||
variant={
|
||||
turret.id === turretDefinition?.id ? "solid" : undefined
|
||||
}
|
||||
key={turret.id}
|
||||
module="turret"
|
||||
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
|
||||
onClick={() => {
|
||||
onSelect?.(tankDefinition, turret);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{turretDefinition && turretDefinition.guns.length > 1 && (
|
||||
<Flex gap="2">
|
||||
{turretDefinition.guns.map((gun) => (
|
||||
<ModuleButton
|
||||
variant={
|
||||
gun.id === gunDefinition?.id ? "solid" : undefined
|
||||
}
|
||||
key={gun.id}
|
||||
module="gun"
|
||||
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
||||
secondaryDiscriminator={
|
||||
<Text style={{ fontSize: "0.75em" }}>
|
||||
{literals(strings.common.units.mm, {
|
||||
value: gun.shells[0].caliber.toFixed(0),
|
||||
})}
|
||||
</Text>
|
||||
}
|
||||
onClick={() => {
|
||||
onSelect?.(tankDefinition, turretDefinition, gun);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function ModuleSeparator() {
|
||||
return (
|
||||
<Text size="5" color="gray" trim="end">
|
||||
<CaretRightIcon width="1em" height="1em" />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const hullTankId = Mixer.use((state) => state.hull);
|
||||
const turretTankId = Mixer.use((state) => state.turret.tank.id);
|
||||
const turretTurretId = Mixer.use((state) => state.turret.turret.id);
|
||||
const gunTankId = Mixer.use((state) => state.gun.tank.id);
|
||||
const gunTurretId = Mixer.use((state) => state.gun.turret.id);
|
||||
const gunGunId = Mixer.use((state) => state.gun.gun.id);
|
||||
const canvas = useRef<HTMLCanvasElement>(null!);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Box flexGrow="1" position="relative">
|
||||
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
||||
{!skeleton && <MixerScene ref={canvas} />}
|
||||
</Box>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
direction="column"
|
||||
position="absolute"
|
||||
bottom="4"
|
||||
left="50%"
|
||||
style={{ transform: "translateX(-50%)" }}
|
||||
gap="4"
|
||||
>
|
||||
<Flex gap="2">
|
||||
<ScreenshotButton
|
||||
name={hullTankId.slug}
|
||||
canvas={canvas}
|
||||
highContrast
|
||||
variant="surface"
|
||||
>
|
||||
{strings.website.tools.mixer.screenshot}
|
||||
</ScreenshotButton>
|
||||
<Button
|
||||
highContrast
|
||||
variant="surface"
|
||||
onClick={() => {
|
||||
const mixed = curateMixer();
|
||||
|
||||
Mixer.mutate((draft) => {
|
||||
Object.assign(draft, mixed);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<UpdateIcon />
|
||||
{strings.website.tools.mixer.randomize}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" align="center">
|
||||
<ModuleChooser
|
||||
skeleton={skeleton}
|
||||
tank={hullTankId.id}
|
||||
onSelect={(tank) => {
|
||||
Mixer.mutate((draft) => {
|
||||
draft.hull = tank;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ModuleSeparator />
|
||||
<ModuleChooser
|
||||
skeleton={skeleton}
|
||||
tank={turretTankId}
|
||||
turret={turretTurretId}
|
||||
onSelect={(tank, turret) => {
|
||||
Mixer.mutate((draft) => {
|
||||
draft.turret = {
|
||||
tank,
|
||||
turret: turret ?? tank.turrets.at(-1)!,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ModuleSeparator />
|
||||
<ModuleChooser
|
||||
skeleton={skeleton}
|
||||
tank={gunTankId}
|
||||
turret={gunTurretId}
|
||||
gun={gunGunId}
|
||||
onSelect={(tank, turret, gun) => {
|
||||
Mixer.mutate((draft) => {
|
||||
draft.gun = {
|
||||
tank,
|
||||
turret: turret ?? tank.turrets.at(-1)!,
|
||||
gun:
|
||||
gun ??
|
||||
turret?.guns.at(-1) ??
|
||||
tank.turrets.at(-1)!.guns.at(-1)!,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import ToolOpenGraph from "../../../components/ToolOpenGraph.astro";
|
||||
import { tools } from "../../../constants/tools";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolOpenGraph {locale} tool={tools.mixer} />
|
||||
|
||||
<BaseLayout {locale} integrated>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import { PlayerCountControl } from "../../../components/Performance/PlayerCountControl";
|
||||
import { TankTable } from "../../../components/Performance/Table";
|
||||
import { FilterControl } from "../../../components/TankSearch/components/FilterControl";
|
||||
import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../hooks/useLocale";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
|
||||
type PageProps = MaybeSkeletonComponentProps & LocaleAcceptorProps;
|
||||
|
||||
export function Page({ skeleton, locale }: PageProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content skeleton={skeleton} />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<PageWrapper pb="0" px="0" color="jade" maxWidth="100%">
|
||||
<FilterControl />
|
||||
<PlayerCountControl />
|
||||
|
||||
<TankTable skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
import ToolOpenGraph from "../../../../components/ToolOpenGraph.astro";
|
||||
import { tools } from "../../../../constants/tools";
|
||||
import BaseLayout from "../../../../layouts/BaseLayout.astro";
|
||||
import type { getStaticPaths } from "../index.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolOpenGraph {locale} tool={tools.performance} />
|
||||
|
||||
<BaseLayout {locale} noFooter>
|
||||
<Page client:only="react" {locale}>
|
||||
<Page skeleton slot="fallback" {locale} />
|
||||
</Page>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import { PlaylistGenerator } from "../../../components/PlaylistGenerator";
|
||||
import { PlaylistOrder } from "../../../components/PlaylistOrder";
|
||||
import { LocaleProvider } from "../../../hooks/useLocale";
|
||||
import { App } from "../../../stores/app";
|
||||
import { Playlist } from "../../../stores/playlist";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
|
||||
interface Props {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function Page({
|
||||
locale,
|
||||
skeleton,
|
||||
}: Props & MaybeSkeletonComponentProps) {
|
||||
const generated = Playlist.use((state) => state.list !== undefined);
|
||||
|
||||
if (App.state.logins.wargaming) TankFilters.state.showUnowned = false;
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<PageWrapper color="tomato" direction="row" maxWidth="unset" p="0">
|
||||
{!generated && <PlaylistGenerator skeleton={skeleton} />}
|
||||
{generated && <PlaylistOrder />}
|
||||
</PageWrapper>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import ToolOpenGraph from "../../../components/ToolOpenGraph.astro";
|
||||
import { tools } from "../../../constants/tools";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import { Page } from "./_index.tsx";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolOpenGraph {locale} tool={tools.playlist} />
|
||||
|
||||
<BaseLayout {locale} noFooter>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,487 @@
|
||||
import {
|
||||
type AccountListWithServer,
|
||||
type CompositeStatsKey,
|
||||
type IndividualAccountInfo,
|
||||
type IndividualTankStats,
|
||||
compositeStats,
|
||||
compositeStatsKeys,
|
||||
deltaTankStats,
|
||||
formatCompositeStat,
|
||||
getAccountInfo,
|
||||
getTankStats,
|
||||
idToRegion,
|
||||
searchPlayersAcrossRegions,
|
||||
sumCompositeStats,
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n";
|
||||
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 {
|
||||
type LocaleAcceptorProps,
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
} from "../../../hooks/useLocale";
|
||||
import { Session, type SessionTracking } from "../../../stores/session";
|
||||
|
||||
const [tankDefinitions, averageDefinitions] = await Promise.all([
|
||||
awaitableTankDefinitions,
|
||||
awaitableAverageDefinitions,
|
||||
]);
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const { locale, strings } = useLocale();
|
||||
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 [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);
|
||||
|
||||
Session.mutate((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>
|
||||
{strings.website.tools.session.no_cc.title}
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
{strings.website.tools.session.no_cc.body}
|
||||
</AlertDialog.Description>
|
||||
|
||||
<Flex mt="4" justify="end">
|
||||
<AlertDialog.Action>
|
||||
<Button variant="solid">
|
||||
{strings.website.tools.session.no_cc.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">
|
||||
{strings.website.tools.session.search.lookup}
|
||||
</Text>
|
||||
<ArrowDownIcon />
|
||||
</Flex>
|
||||
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
size="3"
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "75vw",
|
||||
maxWidth: 480,
|
||||
}}
|
||||
ref={input}
|
||||
placeholder={strings.website.tools.session.search.hint}
|
||||
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">
|
||||
{strings.website.tools.session.search.searching}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!searching && searchResults.length === 0 && (
|
||||
<Flex justify="center">
|
||||
<Text color="gray">
|
||||
{strings.website.tools.session.search.no_results}
|
||||
</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">
|
||||
{literals(strings.website.tools.session.controls.title, {
|
||||
name: accountInfo?.nickname,
|
||||
})}
|
||||
</Heading>
|
||||
<Text color="gray">
|
||||
{literals(strings.website.tools.session.controls.subtitle, {
|
||||
time: new Date(session.player.since).toLocaleString(locale),
|
||||
})}
|
||||
</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;
|
||||
}
|
||||
|
||||
Session.mutate((draft) => {
|
||||
(draft as SessionTracking).player.stats = stats;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{strings.website.tools.session.controls.reset}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
Session.mutate((draft) => {
|
||||
draft.tracking = false;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{strings.website.tools.session.controls.change}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{session.tracking && delta && delta.length > 0 && (
|
||||
<Flex justify="center">
|
||||
<StickyTableRoot variant="surface">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell>
|
||||
{strings.website.tools.session.table.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={() => {
|
||||
Session.mutate((draft) => {
|
||||
(draft as SessionTracking).columns.unshift(
|
||||
key,
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{strings.common.composite_stats[key]}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
)}
|
||||
|
||||
<Select.Root
|
||||
value={column}
|
||||
onValueChange={(value) => {
|
||||
Session.mutate((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>
|
||||
<Select.Item value="remove">
|
||||
{strings.website.tools.session.table.remove}
|
||||
</Select.Item>
|
||||
|
||||
<Select.Separator />
|
||||
|
||||
{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.Content>
|
||||
</Select.Root>
|
||||
</Table.ColumnHeaderCell>
|
||||
))}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
<Table.Row
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<StickyRowHeaderCell
|
||||
style={{
|
||||
paddingLeft: 32,
|
||||
position: "relative",
|
||||
overflowY: "hidden",
|
||||
}}
|
||||
>
|
||||
{strings.website.tools.session.table.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">
|
||||
{strings.website.tools.session.no_battles.title}
|
||||
</Heading>
|
||||
<Text color="gray">
|
||||
{strings.website.tools.session.no_battles.hint}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import { tools } from "../../../constants/tools";
|
||||
import ToolLayout from "../../../layouts/ToolLayout.astro";
|
||||
import type { getStaticPaths } from "../index.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolLayout {locale} tool={tools.session}>
|
||||
<Page client:load {locale} />
|
||||
</ToolLayout>
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Flex, Switch, Text } from "@radix-ui/themes";
|
||||
import { LocaleSwitcher } from "../../../components/LocaleSwitcher";
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import {
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../hooks/useLocale";
|
||||
import { App } from "../../../stores/app";
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content locale={locale} />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content({ locale }: LocaleAcceptorProps) {
|
||||
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<PageWrapper justify="center" align="center">
|
||||
<Flex gap="3" direction="column" width="100%" maxWidth="20rem">
|
||||
<Flex align="center" gap="2" justify="between">
|
||||
<Text>{strings.website.settings.dev_mode}</Text>
|
||||
<Switch
|
||||
variant="classic"
|
||||
checked={developerMode}
|
||||
onCheckedChange={(checked) =>
|
||||
App.mutate((draft) => {
|
||||
draft.developerMode = checked;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" gap="2" justify="between">
|
||||
<Text>{strings.website.settings.language}</Text>
|
||||
<LocaleSwitcher locale={locale} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import { AstroSeo } from "@astrolib/seo";
|
||||
import { getStrings } from "../../../core/i18n/getStrings";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
const strings = getStrings(locale);
|
||||
---
|
||||
|
||||
<AstroSeo
|
||||
title={strings.website.settings.title}
|
||||
description={strings.website.settings.description}
|
||||
/>
|
||||
|
||||
<BaseLayout {locale}>
|
||||
<Page {locale} client:load />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,56 @@
|
||||
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 { GuideSection } from "../../../../components/Tankopedia/GuideSection";
|
||||
import { HeroSection } from "../../../../components/Tankopedia/HeroSection";
|
||||
import { MetaSection } from "../../../../components/Tankopedia/MetaSection";
|
||||
import { TechTreeSection } from "../../../../components/Tankopedia/TechTreeSection";
|
||||
import { VideoSection } from "../../../../components/Tankopedia/VideoSection";
|
||||
import { awaitableModelDefinitions } from "../../../../core/awaitables/modelDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions";
|
||||
import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps";
|
||||
import type { TankGuide } from "../../../../types/tankGuide";
|
||||
|
||||
type PageProps = MaybeSkeletonComponentProps &
|
||||
LocaleAcceptorProps & {
|
||||
id: number;
|
||||
guide?: TankGuide;
|
||||
};
|
||||
|
||||
const [tankDefinitions, modelDefinitions] = await Promise.all([
|
||||
awaitableTankDefinitions,
|
||||
awaitableModelDefinitions,
|
||||
]);
|
||||
|
||||
export function Page({ id, skeleton, locale, guide }: PageProps) {
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
const model = modelDefinitions.models[id];
|
||||
|
||||
Tankopedia.useInitialization(model);
|
||||
Duel.useInitialization({ tank, model });
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9">
|
||||
<HeroSection skeleton={skeleton} />
|
||||
<MetaSection />
|
||||
<CalloutsSection />
|
||||
{tank.type === TankType.TANK_TYPE_RESEARCHABLE && !tank.deprecated && (
|
||||
<TechTreeSection skeleton={skeleton} />
|
||||
)}
|
||||
<CharacteristicsSection skeleton={skeleton} />
|
||||
<GameModeSection />
|
||||
{guide && <GuideSection guide={guide} />}
|
||||
<VideoSection skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
import { AstroSeo } from "@astrolib/seo";
|
||||
import {
|
||||
assertSecret,
|
||||
asset,
|
||||
createDefaultProvisions,
|
||||
createDefaultSkills,
|
||||
fetchEquipmentDefinitions,
|
||||
fetchModelDefinitions,
|
||||
fetchProvisionDefinitions,
|
||||
fetchSkillDefinitions,
|
||||
fetchTankDefinitions,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from "@blitzkit/core";
|
||||
import { unwrapper } from "@blitzkit/i18n";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { load } from "cheerio";
|
||||
import { tankCharacteristics } from "../../../../core/blitzkit/tankCharacteristics";
|
||||
import { getStrings } from "../../../../core/i18n/getStrings";
|
||||
import BaseLayout from "../../../../layouts/BaseLayout.astro";
|
||||
import { genericDefaultEquipmentMatrix } from "../../../../stores/duel/constants";
|
||||
import type { TankGuide } from "../../../../types/tankGuide";
|
||||
import { getStaticPaths as parentGetStaticPaths } from "../index.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const tankDefinitions = await fetchTankDefinitions();
|
||||
const parentPaths = parentGetStaticPaths();
|
||||
|
||||
return Object.values(tankDefinitions.tanks)
|
||||
.map((tank) =>
|
||||
parentPaths.map((parent) => ({
|
||||
params: { ...parent.params, slug: tank.slug },
|
||||
props: { ...parent.props, id: tank.id },
|
||||
})),
|
||||
)
|
||||
.flat();
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { id, locale } = Astro.props;
|
||||
const strings = getStrings(locale);
|
||||
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 unwrap = unwrapper(locale);
|
||||
const title = literals(strings.website.tools.tankopedia.tank_seo_title, {
|
||||
name: unwrap(tank.name!),
|
||||
tier: TIER_ROMAN_NUMERALS[tank.tier],
|
||||
nationality:
|
||||
strings.common.nations_adjectives[
|
||||
tank.nation as keyof typeof strings.common.nations_adjectives
|
||||
],
|
||||
class: strings.common.tank_class_short[tank.class],
|
||||
website: "BlitzKit",
|
||||
});
|
||||
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 shell = gun.shells[0];
|
||||
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,
|
||||
stockEngine: tank.engines[0],
|
||||
stockGun: tank.turrets[0].guns[0],
|
||||
stockTurret: tank.turrets[0],
|
||||
stockTrack: tank.tracks[0],
|
||||
track: tank.tracks.at(-1)!,
|
||||
assaultDistance: 0,
|
||||
equalize: false,
|
||||
},
|
||||
{ equipmentDefinitions, provisionDefinitions, tankModelDefinition },
|
||||
);
|
||||
const description = literals(
|
||||
strings.website.tools.tankopedia.tank_seo_description,
|
||||
{
|
||||
name: unwrap(tank.name!),
|
||||
dpm: Math.round(characteristics.dpm).toLocaleString(locale),
|
||||
alpha: Math.round(characteristics.damage).toLocaleString(locale),
|
||||
penetration: Math.round(characteristics.penetration).toLocaleString(locale),
|
||||
speed: Math.round(characteristics.speedForwards).toLocaleString(locale),
|
||||
health: Math.round(characteristics.health).toLocaleString(locale),
|
||||
},
|
||||
);
|
||||
|
||||
const guideUrl = `${assertSecret(import.meta.env.PUBLIC_GUIDE_WEBSITE)}/${tank.slug}`;
|
||||
let guide: TankGuide | undefined = undefined;
|
||||
const guideResponse = await fetch(guideUrl);
|
||||
|
||||
if (guideResponse.ok) {
|
||||
const $ = await guideResponse.text().then(load);
|
||||
const description = $(".entry-content p").first().text();
|
||||
const image = $(".entry-content img").first().attr("src")?.toString();
|
||||
|
||||
if (description !== undefined || image !== undefined) {
|
||||
guide = {
|
||||
url: guideUrl,
|
||||
description,
|
||||
image,
|
||||
};
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
<AstroSeo
|
||||
{title}
|
||||
{description}
|
||||
openGraph={{ title, description, images: [{ url: icon }], 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 {locale} integrated>
|
||||
<Page {locale} id={id} {guide} client:only="react">
|
||||
<Page {locale} id={id} {guide} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PageWrapper } from "../../../../components/PageWrapper";
|
||||
import { TankSearch } from "../../../../components/TankSearch";
|
||||
import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../../hooks/useLocale";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps";
|
||||
|
||||
export function Page({
|
||||
locale,
|
||||
skeleton,
|
||||
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<PageWrapper color="purple" maxWidth="80rem">
|
||||
<TankSearch skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import { tools } from "../../../../constants/tools";
|
||||
import ToolLayout from "../../../../layouts/ToolLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolLayout {locale} tool={tools.tanks}>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton 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 {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../hooks/useLocale";
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
return (
|
||||
<PageWrapper color="orange" maxWidth="100rem">
|
||||
<URLManager />
|
||||
<TierListControls />
|
||||
<TierListTable />
|
||||
<FilterControl />
|
||||
<TierListTiles />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import { tools } from "../../../constants/tools";
|
||||
import ToolLayout from "../../../layouts/ToolLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolLayout {locale} tool={tools.tier_list}>
|
||||
<Page {locale} client:load />
|
||||
</ToolLayout>
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import LegacyRedirect from "../../../../layouts/LegacyRedirect.astro";
|
||||
|
||||
export { getStaticPaths } from "../../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<LegacyRedirect {locale} path="compare" />
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { embedConfigurations } from "../../../../../constants/embeds";
|
||||
import LegacyRedirect from "../../../../../layouts/LegacyRedirect.astro";
|
||||
import { getStaticPaths as parentGetStaticPaths } from "../index.astro";
|
||||
|
||||
export const getStaticPaths = (() => {
|
||||
const parentPaths = parentGetStaticPaths();
|
||||
|
||||
return Object.keys(embedConfigurations)
|
||||
.map((embed) =>
|
||||
parentPaths.map((parent) => ({
|
||||
params: {
|
||||
...parent.params,
|
||||
embed: embed as keyof typeof embedConfigurations,
|
||||
},
|
||||
}))
|
||||
)
|
||||
.flat();
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { embed } = Astro.params;
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<LegacyRedirect {locale} path={`embed/${embed}`} />
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import LegacyRedirect from "../../../../layouts/LegacyRedirect.astro";
|
||||
|
||||
export { getStaticPaths } from "../../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<LegacyRedirect {locale} path="embed" />
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import LegacyRedirect from "../../../../layouts/LegacyRedirect.astro";
|
||||
|
||||
export { getStaticPaths } from "../../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<LegacyRedirect {locale} path="gallery" />
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import LegacyRedirect from "../../../../layouts/LegacyRedirect.astro";
|
||||
|
||||
export { getStaticPaths } from "../../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<LegacyRedirect {locale} path="performance" />
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import LegacyRedirect from "../../../../layouts/LegacyRedirect.astro";
|
||||
|
||||
export { getStaticPaths } from "../../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<LegacyRedirect {locale} path="session" />
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
import { fetchTankDefinitions } from "@blitzkit/core";
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import LegacyRedirect from "../../../../../layouts/LegacyRedirect.astro";
|
||||
import { getStaticPaths as parentGetStaticPaths } from "../index.astro";
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const tankDefinitions = await fetchTankDefinitions();
|
||||
const parentPaths = parentGetStaticPaths();
|
||||
|
||||
return Object.values(tankDefinitions.tanks)
|
||||
.map((tank) =>
|
||||
parentPaths.map((parent) => ({
|
||||
params: { ...parent.params, id: tank.id },
|
||||
props: parent.props,
|
||||
})),
|
||||
)
|
||||
.flat();
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { id } = Astro.params;
|
||||
const { locale } = Astro.props;
|
||||
const tankDefinitions = await fetchTankDefinitions();
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
---
|
||||
|
||||
<LegacyRedirect {locale} path={`tanks/${tank.slug}`} />
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import LegacyRedirect from "../../../../layouts/LegacyRedirect.astro";
|
||||
|
||||
export { getStaticPaths } from "../../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<LegacyRedirect {locale} path="tanks" />
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import LegacyRedirect from "../../../../layouts/LegacyRedirect.astro";
|
||||
|
||||
export { getStaticPaths } from "../../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<LegacyRedirect {locale} path="tier-list" />
|
||||
Reference in new issue
Block a user