mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: 404 page
This commit is contained in:
1 parent
5adf90bab2
commit
bc6bb4f10e
28 files changed
+1505
-97
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
|
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
|
||||||
import { deburr } from 'lodash-es';
|
import { deburr } from 'lodash-es';
|
||||||
import { SUPPORTED_LOCALES } from '../../../i18n/src/strings';
|
|
||||||
import { I18nString } from '../protos';
|
import { I18nString } from '../protos';
|
||||||
import { fetchCamouflageDefinitions } from './camouflageDefinitions';
|
import { fetchCamouflageDefinitions } from './camouflageDefinitions';
|
||||||
import { fetchTankDefinitions } from './tankDefinitions';
|
import { fetchTankDefinitions } from './tankDefinitions';
|
||||||
@@ -26,8 +26,8 @@ export async function fetchTankNames() {
|
|||||||
searchableNameDeburr,
|
searchableNameDeburr,
|
||||||
camouflages: tank.camouflages
|
camouflages: tank.camouflages
|
||||||
?.map((id) =>
|
?.map((id) =>
|
||||||
SUPPORTED_LOCALES.map(
|
locales.supported.map(
|
||||||
(locale) =>
|
({ locale }) =>
|
||||||
camouflageDefinitions.camouflages[id]?.name.locales[locale],
|
camouflageDefinitions.camouflages[id]?.name.locales[locale],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -42,9 +42,11 @@ export async function fetchTankNames() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SEARCH_KEYS = [
|
export const SEARCH_KEYS = [
|
||||||
...SUPPORTED_LOCALES.map((locale) => [
|
...locales.supported
|
||||||
`searchableName.locales.${locale}`,
|
.map(({ locale }) => [
|
||||||
`searchableNameDeburr.locales.${locale}`,
|
`searchableName.locales.${locale}`,
|
||||||
]).flat(),
|
`searchableNameDeburr.locales.${locale}`,
|
||||||
|
])
|
||||||
|
.flat(),
|
||||||
'camouflages',
|
'camouflages',
|
||||||
];
|
];
|
||||||
@@ -1,89 +1,8 @@
|
|||||||
import { merge } from 'ts-deepmerge';
|
import type en from '../strings/en.json';
|
||||||
import en from '../strings/en.json';
|
|
||||||
import es from '../strings/es.json';
|
|
||||||
import fr from '../strings/fr.json';
|
|
||||||
import ja from '../strings/ja.json';
|
|
||||||
import pl from '../strings/pl.json';
|
|
||||||
import pt from '../strings/pt.json';
|
|
||||||
import ru from '../strings/ru.json';
|
|
||||||
import uk from '../strings/uk.json';
|
|
||||||
import zh from '../strings/zh.json';
|
|
||||||
|
|
||||||
export type BlitzKitStrings = typeof en;
|
export type BlitzKitStrings = typeof en;
|
||||||
|
|
||||||
const stringsRaw = { en, es, fr, ja, pl, pt, ru, uk, zh };
|
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
|
||||||
|
|
||||||
export const SUPPORTED_LOCALES = Object.keys(stringsRaw) as SupportedLocale[];
|
|
||||||
|
|
||||||
export const DEFAULT_LOCALE = SUPPORTED_LOCALES[0];
|
|
||||||
|
|
||||||
function redact(string: string) {
|
|
||||||
const matches = [...string.matchAll(/%s\d+/g)];
|
|
||||||
const characters = string
|
|
||||||
.split('')
|
|
||||||
.map((char) => (char === ' ' ? ' ' : '█')) as string[];
|
|
||||||
|
|
||||||
matches.forEach((match) => {
|
|
||||||
const start = match.index;
|
|
||||||
const matchText = match[0];
|
|
||||||
for (let i = 0; i < matchText.length; i++) {
|
|
||||||
characters[start + i] = matchText[i];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return characters.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function nuke(object: TranslationTree) {
|
|
||||||
for (const key in object) {
|
|
||||||
const typedKey = key as keyof typeof object;
|
|
||||||
|
|
||||||
if (typeof object[typedKey] === 'string') {
|
|
||||||
const value = object[typedKey];
|
|
||||||
object[typedKey] = redact(value as string);
|
|
||||||
} else nuke(object[typedKey] as TranslationTree);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stringsPartial: Partial<Record<SupportedLocale, TranslationTree>> = {};
|
|
||||||
|
|
||||||
for (const locale in stringsRaw) {
|
|
||||||
if (locale === DEFAULT_LOCALE) {
|
|
||||||
stringsPartial[DEFAULT_LOCALE] = stringsRaw[DEFAULT_LOCALE];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localeStrings = stringsRaw[locale as SupportedLocale];
|
|
||||||
stringsPartial[locale as SupportedLocale] = merge(
|
|
||||||
stringsRaw[DEFAULT_LOCALE],
|
|
||||||
localeStrings,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (import.meta.env.PUBLIC_DEBUG_MISSING_I18N === 'true') {
|
|
||||||
nuke(stringsPartial[locale as SupportedLocale]!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const STRINGS = stringsPartial as Record<
|
|
||||||
SupportedLocale,
|
|
||||||
BlitzKitStrings
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type SupportedLocale = keyof typeof stringsRaw;
|
|
||||||
|
|
||||||
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<SupportedLocale, string> = {
|
|
||||||
en: 'en',
|
|
||||||
es: 'es',
|
|
||||||
fr: 'fr',
|
|
||||||
ja: 'ja',
|
|
||||||
pt: 'pt',
|
|
||||||
ru: 'ru',
|
|
||||||
uk: 'uk',
|
|
||||||
zh: 'zh-Hans',
|
|
||||||
pl: 'pl',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SUPPORTED_LOCALE_FLAGS: Record<SupportedLocale, string> = {
|
|
||||||
en: '🇺🇸',
|
en: '🇺🇸',
|
||||||
es: '🇪🇸',
|
es: '🇪🇸',
|
||||||
fr: '🇫🇷',
|
fr: '🇫🇷',
|
||||||
@@ -95,6 +14,14 @@ export const SUPPORTED_LOCALE_FLAGS: Record<SupportedLocale, string> = {
|
|||||||
pl: '🇵🇱',
|
pl: '🇵🇱',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TranslationTree = { [key: string]: TranslationNode };
|
export const LOCALE_NAMES: Record<string, string> = {
|
||||||
|
en: 'English',
|
||||||
export type TranslationNode = string | TranslationTree;
|
es: 'Español',
|
||||||
|
fr: 'Français',
|
||||||
|
ja: '日本語',
|
||||||
|
pt: 'Português',
|
||||||
|
ru: 'Русский',
|
||||||
|
uk: 'Українська',
|
||||||
|
zh: '中文',
|
||||||
|
pl: 'Polski',
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { I18nString } from '@blitzkit/core';
|
import { I18nString } from '@blitzkit/core';
|
||||||
import { DEFAULT_LOCALE } from './strings';
|
import locales from '../locales.json' with { type: 'json' };
|
||||||
|
|
||||||
export function unwrapper(locale: string) {
|
export function unwrapper(locale: string) {
|
||||||
return function (i18nString: I18nString) {
|
return function (i18nString: I18nString) {
|
||||||
@@ -7,6 +7,6 @@ export function unwrapper(locale: string) {
|
|||||||
return i18nString.locales[locale];
|
return i18nString.locales[locale];
|
||||||
}
|
}
|
||||||
|
|
||||||
return i18nString.locales[DEFAULT_LOCALE];
|
return i18nString.locales[locales.default];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -5,5 +5,6 @@ import react from '@astrojs/react';
|
|||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [react()]
|
integrations: [react()],
|
||||||
});
|
devToolbar: { enabled: false },
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Theme, type ThemeProps } from '@radix-ui/themes';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface BlitzKitThemeProps extends ThemeProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlitzKitTheme({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: BlitzKitThemeProps) {
|
||||||
|
return (
|
||||||
|
<Theme
|
||||||
|
style={{ minHeight: 'unset', display: 'contents', ...style }}
|
||||||
|
className={`blitzkit-theme ${className}`}
|
||||||
|
grayColor="mauve"
|
||||||
|
appearance="dark"
|
||||||
|
panelBackground="solid"
|
||||||
|
radius="full"
|
||||||
|
accentColor="amber"
|
||||||
|
suppressHydrationWarning
|
||||||
|
suppressContentEditableWarning
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Theme>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.policies-agreement {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--color-overlay);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policies-agreement .content-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--color-panel-solid);
|
||||||
|
}
|
||||||
|
|
||||||
|
.policies-agreement .content {
|
||||||
|
max-width: 60rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { assertSecret, idToRegion } from '@blitzkit/core';
|
||||||
|
import { Button, Flex, Heading, Text } from '@radix-ui/themes';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
LocaleProvider,
|
||||||
|
useLocale,
|
||||||
|
type LocaleAcceptorProps,
|
||||||
|
} from '../../hooks/useLocale';
|
||||||
|
import { App } from '../../stores/app';
|
||||||
|
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../../stores/app/constants';
|
||||||
|
|
||||||
|
import { LinkI18n } from '../LinkI18n';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
type Extension =
|
||||||
|
| {
|
||||||
|
status: 'ok';
|
||||||
|
data: {
|
||||||
|
access_token: string;
|
||||||
|
account_id: number;
|
||||||
|
expires_at: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: 'error';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Checks({ locale }: LocaleAcceptorProps) {
|
||||||
|
return (
|
||||||
|
<LocaleProvider locale={locale}>
|
||||||
|
<App.Provider>
|
||||||
|
<Content />
|
||||||
|
</App.Provider>
|
||||||
|
</LocaleProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wargaming: 14 (refresh: 7)
|
||||||
|
*/
|
||||||
|
function Content() {
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const logins = App.use((state) => state.logins);
|
||||||
|
const mutateApp = App.useMutation();
|
||||||
|
const appStore = App.useStore();
|
||||||
|
const policiesAgreementIndex = App.use(
|
||||||
|
(state) => state.policiesAgreementIndex,
|
||||||
|
);
|
||||||
|
const isLegal = window.location.pathname.startsWith('/legal');
|
||||||
|
const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function unsubscribe() {
|
||||||
|
window.removeEventListener('scroll', show);
|
||||||
|
window.removeEventListener('pointermove', show);
|
||||||
|
window.removeEventListener('pointerdown', show);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
setShowPoliciesAgreement(true);
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
|
||||||
|
!isLegal
|
||||||
|
) {
|
||||||
|
window.addEventListener('scroll', show);
|
||||||
|
window.addEventListener('pointermove', show);
|
||||||
|
window.addEventListener('pointerdown', show);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [policiesAgreementIndex, isLegal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logins.wargaming) {
|
||||||
|
const expiresIn = logins.wargaming.expires - Date.now();
|
||||||
|
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
||||||
|
|
||||||
|
if (expiresInDays < 0) {
|
||||||
|
mutateApp((draft) => {
|
||||||
|
draft.logins.wargaming = undefined;
|
||||||
|
});
|
||||||
|
} else if (expiresInDays < 7) {
|
||||||
|
const { wargaming } = appStore.getState().logins;
|
||||||
|
|
||||||
|
if (!wargaming) return;
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
`https://api.worldoftanks.${idToRegion(
|
||||||
|
wargaming.id,
|
||||||
|
)}/wot/auth/prolongate/?application_id=${assertSecret(
|
||||||
|
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID,
|
||||||
|
)}&access_token=${wargaming.token}`,
|
||||||
|
)
|
||||||
|
.then((response) => response.json() as Promise<Extension>)
|
||||||
|
.then((json) => {
|
||||||
|
mutateApp((draft) => {
|
||||||
|
if (json.status === 'error') {
|
||||||
|
draft.logins.wargaming = undefined;
|
||||||
|
} else {
|
||||||
|
draft.logins.wargaming = {
|
||||||
|
id: wargaming.id,
|
||||||
|
token: json.data.access_token,
|
||||||
|
expires: json.data.expires_at * 1000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!showPoliciesAgreement) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex className="policies-agreement" align="end">
|
||||||
|
<Flex p="8" className="content-wrapper" justify="center">
|
||||||
|
<Flex align="start" gap="4" className="content" direction="column">
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<Heading>
|
||||||
|
{policiesAgreementIndex === -1
|
||||||
|
? "BlitzKit's policies"
|
||||||
|
: "BlitzKit's policies have changed"}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
This website utilizes cookies to perform analytics and personalize
|
||||||
|
your experience. You can learn more through{' '}
|
||||||
|
<LinkI18n locale={locale} href="/docs/legal/privacy-policy">
|
||||||
|
our privacy policy
|
||||||
|
</LinkI18n>
|
||||||
|
. By using BlitzKit, you also agree to our{' '}
|
||||||
|
<LinkI18n locale={locale} href="/docs/legal/terms-of-service">
|
||||||
|
terms of service
|
||||||
|
</LinkI18n>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
mt="2"
|
||||||
|
onClick={() => {
|
||||||
|
appStore.setState({
|
||||||
|
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX,
|
||||||
|
});
|
||||||
|
setShowPoliciesAgreement(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
I agree
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
---
|
||||||
|
import { assertSecret, fetchGameDefinitions } from '@blitzkit/core';
|
||||||
|
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
|
||||||
|
import { Box, Code, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||||
|
import packageJson from '../../../../package.json';
|
||||||
|
import { getStrings } from '../core/i18n/getStrings';
|
||||||
|
import { LinkI18n } from './LinkI18n';
|
||||||
|
import { LocaleSwitcherThemeWrapper } from './LocaleSwitcher';
|
||||||
|
|
||||||
|
const gameDefinitions = await fetchGameDefinitions();
|
||||||
|
const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||||
|
const locale = Astro.params.locale ?? locales.default;
|
||||||
|
const strings = await getStrings(locale);
|
||||||
|
---
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.footer {
|
||||||
|
background-color: var(--color-panel);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<Flex className="footer" gap="6" p="6" direction="column" align="center">
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="between"
|
||||||
|
maxWidth="40rem"
|
||||||
|
width="100%"
|
||||||
|
flexGrow="1"
|
||||||
|
gap="5"
|
||||||
|
position="relative"
|
||||||
|
direction={{ initial: 'column', sm: 'row' }}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
align={{
|
||||||
|
initial: 'center',
|
||||||
|
sm: 'start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading>BlitzKit</Heading>
|
||||||
|
<Text color="gray">{strings.website.footer.motto}</Text>
|
||||||
|
<Text size="1" color="gray">
|
||||||
|
{strings.website.footer.copyright}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box mt="2">
|
||||||
|
<LocaleSwitcherThemeWrapper {locale} client:load />
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
gap="6"
|
||||||
|
width={{
|
||||||
|
initial: '100%',
|
||||||
|
sm: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
gap="1"
|
||||||
|
align={{
|
||||||
|
initial: 'end',
|
||||||
|
sm: 'start',
|
||||||
|
}}
|
||||||
|
flexGrow={{
|
||||||
|
initial: '1',
|
||||||
|
sm: '0',
|
||||||
|
}}
|
||||||
|
flexBasis={{
|
||||||
|
initial: '0',
|
||||||
|
sm: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading size="3">{strings.website.footer.about.title}</Heading>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
align={{
|
||||||
|
initial: 'end',
|
||||||
|
sm: 'start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinkI18n {locale} href="/docs/about/credits" color="gray">
|
||||||
|
{strings.website.footer.about.credits}
|
||||||
|
</LinkI18n>
|
||||||
|
<Link
|
||||||
|
href="https://discord.gg/nDt7AjGJQH"
|
||||||
|
target="_blank"
|
||||||
|
color="gray"
|
||||||
|
>
|
||||||
|
{strings.website.footer.about.discord}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="https://github.com/tresabhi/blitzkit"
|
||||||
|
target="_blank"
|
||||||
|
color="gray"
|
||||||
|
>
|
||||||
|
{strings.website.footer.about.github}
|
||||||
|
</Link>
|
||||||
|
<LinkI18n {locale} href="/docs/patches" color="gray">
|
||||||
|
{strings.website.footer.about.patches}
|
||||||
|
</LinkI18n>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
gap="1"
|
||||||
|
flexGrow={{
|
||||||
|
initial: '1',
|
||||||
|
sm: '0',
|
||||||
|
}}
|
||||||
|
flexBasis={{
|
||||||
|
initial: '0',
|
||||||
|
sm: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading size="3">{strings.website.footer.legal.title}</Heading>
|
||||||
|
<Flex direction="column">
|
||||||
|
<LinkI18n {locale} href="/docs/legal/privacy-policy" color="gray">
|
||||||
|
{strings.website.footer.legal.privacy}
|
||||||
|
</LinkI18n>
|
||||||
|
<LinkI18n {locale} href="/docs/legal/terms-of-service" color="gray">
|
||||||
|
{strings.website.footer.legal.terms}
|
||||||
|
</LinkI18n>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="5" display={{ initial: 'flex', sm: 'none' }}>
|
||||||
|
<Flex direction="column" justify="between" align="end">
|
||||||
|
<Text size="2" color="gray">BlitzKit</Text>
|
||||||
|
{
|
||||||
|
PUBLIC_ASSET_BRANCH !== 'preview' && (
|
||||||
|
<Text size="2" color="gray">
|
||||||
|
WoTB
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Text size="2" color="gray">CDN</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex direction="column" justify="between" align="start">
|
||||||
|
<Code size="2" color="gray">
|
||||||
|
{packageJson.version}
|
||||||
|
</Code>
|
||||||
|
{
|
||||||
|
PUBLIC_ASSET_BRANCH !== 'preview' && (
|
||||||
|
<Code size="2" color="gray">
|
||||||
|
{gameDefinitions.version}
|
||||||
|
</Code>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Code size="2" color="gray">
|
||||||
|
{PUBLIC_ASSET_BRANCH}
|
||||||
|
</Code>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="2" align="center" display={{ initial: 'none', sm: 'flex' }}>
|
||||||
|
<Text size="2" color="gray">BlitzKit</Text>
|
||||||
|
<Code size="2" color="gray">
|
||||||
|
{packageJson.version}
|
||||||
|
</Code>
|
||||||
|
|
||||||
|
{
|
||||||
|
PUBLIC_ASSET_BRANCH !== 'preview' && (
|
||||||
|
<>
|
||||||
|
<Text size="2" color="gray">
|
||||||
|
WoTB
|
||||||
|
</Text>
|
||||||
|
<Code size="2" color="gray">
|
||||||
|
{gameDefinitions.version}
|
||||||
|
</Code>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<Text size="2" color="gray">CDN</Text>
|
||||||
|
<Code size="2" color="gray">
|
||||||
|
{PUBLIC_ASSET_BRANCH}
|
||||||
|
</Code>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
|
||||||
|
import { Link, type LinkProps } from '@radix-ui/themes';
|
||||||
|
import type { LocaleAcceptorProps } from '../hooks/useLocale';
|
||||||
|
|
||||||
|
export function LinkI18n({
|
||||||
|
locale,
|
||||||
|
href,
|
||||||
|
...props
|
||||||
|
}: LinkProps & LocaleAcceptorProps) {
|
||||||
|
const prefix = locale === locales.default ? '' : `/${locale}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={href?.startsWith('/') ? `${prefix}${href}` : href} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
|
||||||
|
import { Select } from '@radix-ui/themes';
|
||||||
|
import { LOCALE_NAMES, SUPPORTED_LOCALE_FLAGS } from 'packages/i18n/src';
|
||||||
|
import type { LocaleAcceptorProps } from '../hooks/useLocale';
|
||||||
|
import { BlitzKitTheme } from './BlitzKitTheme';
|
||||||
|
|
||||||
|
export function LocaleSwitcherThemeWrapper({ locale }: LocaleAcceptorProps) {
|
||||||
|
return (
|
||||||
|
<BlitzKitTheme style={{ background: 'transparent' }}>
|
||||||
|
<LocaleSwitcher locale={locale} />
|
||||||
|
</BlitzKitTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocaleSwitcher({ locale }: LocaleAcceptorProps) {
|
||||||
|
return (
|
||||||
|
<Select.Root
|
||||||
|
defaultValue={locale}
|
||||||
|
onValueChange={(locale) => {
|
||||||
|
localStorage.setItem('preferred-locale', locale);
|
||||||
|
|
||||||
|
let rawPath = window.location.pathname;
|
||||||
|
|
||||||
|
for (const supportedLocale of locales.supported) {
|
||||||
|
if (
|
||||||
|
window.location.pathname.startsWith(`/${supportedLocale.locale}`)
|
||||||
|
) {
|
||||||
|
rawPath = rawPath.replace(`/${supportedLocale.locale}`, '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.pathname = `${
|
||||||
|
locale === locales.default ? '' : `/${locale}`
|
||||||
|
}${rawPath}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Trigger variant="classic" />
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
{locales.supported.map(({ locale }) => (
|
||||||
|
<Select.Item key={locale} value={locale}>
|
||||||
|
{SUPPORTED_LOCALE_FLAGS[locale]} {LOCALE_NAMES[locale]}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
---
|
||||||
|
import { assertSecret } from '@blitzkit/core';
|
||||||
|
import {
|
||||||
|
Cross1Icon,
|
||||||
|
DiscordLogoIcon,
|
||||||
|
GearIcon,
|
||||||
|
HamburgerMenuIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
|
import { Box, Code, Flex, IconButton, Link } from '@radix-ui/themes';
|
||||||
|
import { TOOLS } from '../constants/tools';
|
||||||
|
import { resolveBranchName } from '../core/blitzkit/branches';
|
||||||
|
import { getStrings } from '../core/i18n/getStrings';
|
||||||
|
import { Var } from '../core/radix/var';
|
||||||
|
import { LinkI18n } from './LinkI18n';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
integrated?: boolean;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { integrated, locale } = Astro.props;
|
||||||
|
const toolsFiltered = TOOLS.filter((tool) => !('href' in tool));
|
||||||
|
const branchName = resolveBranchName(locale);
|
||||||
|
const isBranchNamed = branchName !== undefined;
|
||||||
|
const strings = await getStrings(locale);
|
||||||
|
---
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.navbar,
|
||||||
|
.padder {
|
||||||
|
transition-duration: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.no-navbar .navbar {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.no-navbar .padder {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar[data-expanded='false'] *[data-expanded] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar[data-expanded='true'] {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar[data-expanded='false'] .content {
|
||||||
|
background-color: var(--black-a8);
|
||||||
|
backdrop-filter: blur(4rem);
|
||||||
|
box-shadow: var(--shadow-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar[data-expanded='true'] .content {
|
||||||
|
background-color: var(--gray-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar[data-expanded='true'] *[data-closed] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const navbar = document.querySelector<HTMLDivElement>('.navbar');
|
||||||
|
const hamburger = document.querySelector<HTMLButtonElement>('.hamburger');
|
||||||
|
|
||||||
|
navbar?.addEventListener('click', () => {
|
||||||
|
navbar.dataset.expanded = 'false';
|
||||||
|
});
|
||||||
|
hamburger?.addEventListener('click', (event) => {
|
||||||
|
if (!navbar) return;
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
navbar.dataset.expanded = `${navbar.dataset.expanded === 'false'}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{
|
||||||
|
!integrated && (
|
||||||
|
<Box className="padder" height={{ initial: '3.5rem', sm: '3rem' }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
align="center"
|
||||||
|
data-expanded="false"
|
||||||
|
className="navbar"
|
||||||
|
width="100%"
|
||||||
|
top="0"
|
||||||
|
position="fixed"
|
||||||
|
style={{
|
||||||
|
zIndex: 1,
|
||||||
|
backdropFilter: 'blur(4rem)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
className="content"
|
||||||
|
width="100%"
|
||||||
|
p={{ initial: '4', sm: '3' }}
|
||||||
|
direction="column"
|
||||||
|
align="center"
|
||||||
|
style={{ backgroundColor: integrated ? undefined : Var('gray-1') }}
|
||||||
|
>
|
||||||
|
<Flex gap="4" direction="column" maxWidth="80rem" width="100%">
|
||||||
|
<Flex align="center">
|
||||||
|
<LinkI18n
|
||||||
|
{locale}
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
href="/"
|
||||||
|
underline="hover"
|
||||||
|
weight="medium"
|
||||||
|
>
|
||||||
|
<Flex align="center" gap="1">
|
||||||
|
BlitzKit
|
||||||
|
{
|
||||||
|
isBranchNamed && (
|
||||||
|
<Code color="gray" size="1" variant="outline">
|
||||||
|
{branchName}
|
||||||
|
</Code>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
</LinkI18n>
|
||||||
|
|
||||||
|
<Box flexGrow="1" />
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
gap={{ initial: '4', md: '6' }}
|
||||||
|
display={{ initial: 'none', sm: 'flex' }}
|
||||||
|
justify="center"
|
||||||
|
className="tools"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
toolsFiltered.map((tool) => {
|
||||||
|
const unavailableOnBranch = tool.branches?.every(
|
||||||
|
(branch) =>
|
||||||
|
branch !== assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
|
||||||
|
);
|
||||||
|
const selected = Astro.url.pathname.includes(`/${tool.id}`);
|
||||||
|
const toolStrings =
|
||||||
|
strings.website.tools[
|
||||||
|
(tool.stringsId ??
|
||||||
|
tool.id) as keyof typeof strings.website.tools
|
||||||
|
];
|
||||||
|
|
||||||
|
if (unavailableOnBranch) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinkI18n
|
||||||
|
{locale}
|
||||||
|
weight={selected ? 'medium' : 'regular'}
|
||||||
|
color="gray"
|
||||||
|
highContrast={selected}
|
||||||
|
size="2"
|
||||||
|
href={`/${tool.id}`}
|
||||||
|
underline="hover"
|
||||||
|
>
|
||||||
|
{toolStrings.name}
|
||||||
|
</LinkI18n>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Box flexGrow="1" />
|
||||||
|
|
||||||
|
<Flex align="center" gap="4">
|
||||||
|
<Link
|
||||||
|
style={{ display: 'flex', alignContent: 'center' }}
|
||||||
|
color="gray"
|
||||||
|
href="https://discord.gg/nDt7AjGJQH"
|
||||||
|
underline="none"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<DiscordLogoIcon width="1em" height="1em" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<LinkI18n
|
||||||
|
{locale}
|
||||||
|
style={{ display: 'flex', alignContent: 'center' }}
|
||||||
|
color="gray"
|
||||||
|
href="/settings"
|
||||||
|
underline="none"
|
||||||
|
>
|
||||||
|
<GearIcon width="1em" height="1em" />
|
||||||
|
</LinkI18n>
|
||||||
|
|
||||||
|
<!-- <NavbarAccount client:idle /> -->
|
||||||
|
|
||||||
|
<Flex align="center" display={{ initial: 'flex', sm: 'none' }}>
|
||||||
|
<IconButton variant="ghost" color="gray" className="hamburger">
|
||||||
|
<Cross1Icon data-expanded />
|
||||||
|
<HamburgerMenuIcon data-closed />
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
data-expanded
|
||||||
|
gap="3"
|
||||||
|
display={{ initial: 'flex', sm: 'none' }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
toolsFiltered.map((tool) => {
|
||||||
|
const unavailableOnBranch = tool.branches?.every(
|
||||||
|
(branch) =>
|
||||||
|
branch !== assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unavailableOnBranch) return null;
|
||||||
|
|
||||||
|
const toolStrings =
|
||||||
|
strings.website.tools[
|
||||||
|
(tool.stringsId ??
|
||||||
|
tool.id) as keyof typeof strings.website.tools
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
style={{ display: 'contents' }}
|
||||||
|
href={`/${tool.id}`}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
borderRadius: Var('radius-2'),
|
||||||
|
backgroundImage: `url(/assets/images/${tool.id}.webp)`,
|
||||||
|
backgroundSize: '200%',
|
||||||
|
backgroundPosition: 'right center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
py="4"
|
||||||
|
justify="end"
|
||||||
|
flexGrow="1"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, transparent, ${Var('gray-1')} 90%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{toolStrings.name}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
data-expanded
|
||||||
|
flexGrow="1"
|
||||||
|
width="100%"
|
||||||
|
display={{ initial: 'block', sm: 'none' }}
|
||||||
|
style={{
|
||||||
|
backgroundColor: Var('black-a7'),
|
||||||
|
backdropFilter: 'blur(0.5rem)',
|
||||||
|
WebkitBackdropFilter: 'blur(0.5rem)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Flex, type FlexProps, Theme } from '@radix-ui/themes';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import type { RadixSize } from '../stores/embedState';
|
||||||
|
import { BlitzKitTheme } from './BlitzKitTheme';
|
||||||
|
|
||||||
|
type PageWrapperProps = FlexProps & {
|
||||||
|
color?: ComponentProps<typeof Theme>['accentColor'];
|
||||||
|
containerProps?: ComponentProps<typeof Theme>;
|
||||||
|
noFlex1?: boolean;
|
||||||
|
padding?: RadixSize;
|
||||||
|
noMinHeight?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PageWrapper({
|
||||||
|
style,
|
||||||
|
color,
|
||||||
|
children,
|
||||||
|
noFlex1 = false,
|
||||||
|
containerProps,
|
||||||
|
noMinHeight = false,
|
||||||
|
padding = '4',
|
||||||
|
...props
|
||||||
|
}: PageWrapperProps) {
|
||||||
|
return (
|
||||||
|
<BlitzKitTheme
|
||||||
|
accentColor={color}
|
||||||
|
style={{
|
||||||
|
flex: noFlex1 ? undefined : 1,
|
||||||
|
display: 'flex',
|
||||||
|
minHeight: noMinHeight ? 'unset' : 'calc(100svh - 3rem)',
|
||||||
|
flexDirection: 'column',
|
||||||
|
...containerProps?.style,
|
||||||
|
}}
|
||||||
|
{...containerProps}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
gap="4"
|
||||||
|
p={padding}
|
||||||
|
width="100%"
|
||||||
|
maxWidth="80rem"
|
||||||
|
style={{
|
||||||
|
margin: 'auto',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
flex: 1,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
</BlitzKitTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import { assertSecret } from '@blitzkit/core';
|
||||||
|
|
||||||
|
const PUBLIC_GOOGLE_TAG_MANAGER_ID = assertSecret(
|
||||||
|
import.meta.env.PUBLIC_GOOGLE_TAG_MANAGER_ID,
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* google analytics */}
|
||||||
|
<script
|
||||||
|
is:inline
|
||||||
|
async
|
||||||
|
type="text/partytown"
|
||||||
|
src={`https://www.googletagmanager.com/gtag/js?id=${PUBLIC_GOOGLE_TAG_MANAGER_ID}`}
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
is:inline
|
||||||
|
define:vars={{ PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
||||||
|
type="text/partytown"
|
||||||
|
>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
|
||||||
|
function gtag() {
|
||||||
|
dataLayer.push(arguments);
|
||||||
|
}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', PUBLIC_GOOGLE_TAG_MANAGER_ID);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{/* nitro ads */}
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
<script is:inline data-cfasync="false">
|
||||||
|
window.nitroAds = window.nitroAds || {
|
||||||
|
createAd: function () {
|
||||||
|
return new Promise((e) => {
|
||||||
|
window.nitroAds.queue.push(['createAd', arguments, e]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addUserToken: function () {
|
||||||
|
window.nitroAds.queue.push(['addUserToken', arguments]);
|
||||||
|
},
|
||||||
|
queue: [],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
*/
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { Button } from '@radix-ui/themes';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export interface Tool {
|
||||||
|
id: string;
|
||||||
|
stringsId?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
href?: string;
|
||||||
|
button: {
|
||||||
|
highContrast?: boolean;
|
||||||
|
color: ComponentProps<typeof Button>['color'];
|
||||||
|
};
|
||||||
|
significant?: boolean;
|
||||||
|
branches?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tankopediaTool: Tool = {
|
||||||
|
id: 'tanks',
|
||||||
|
stringsId: 'tankopedia',
|
||||||
|
button: {
|
||||||
|
color: 'purple',
|
||||||
|
},
|
||||||
|
significant: true,
|
||||||
|
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const ratingTool: Tool = {
|
||||||
|
// id: 'rating',
|
||||||
|
// image: 'WEyMZH3',
|
||||||
|
// button: {
|
||||||
|
// color: 'orange',
|
||||||
|
// },
|
||||||
|
// branches: ['main', 'dev'],
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const compareTool: Tool = {
|
||||||
|
id: 'compare',
|
||||||
|
button: {
|
||||||
|
color: 'crimson',
|
||||||
|
},
|
||||||
|
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sessionTool: Tool = {
|
||||||
|
// TODO: rename to tracker
|
||||||
|
id: 'session',
|
||||||
|
button: {
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
branches: ['main', 'dev'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const discordTool: Tool = {
|
||||||
|
id: 'discord',
|
||||||
|
href: 'https://discord.com/application-directory/1097673957865443370',
|
||||||
|
button: {
|
||||||
|
color: 'indigo',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moreTool: Tool = {
|
||||||
|
id: 'more',
|
||||||
|
href: 'https://discord.gg/nDt7AjGJQH',
|
||||||
|
button: {
|
||||||
|
color: 'plum',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const playersTool: Tool = {
|
||||||
|
id: 'players',
|
||||||
|
button: {
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
href: '_', // TODO: remove this to re-enable link
|
||||||
|
branches: ['main', 'dev'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const performanceTool: Tool = {
|
||||||
|
id: 'performance',
|
||||||
|
button: {
|
||||||
|
color: 'jade',
|
||||||
|
},
|
||||||
|
branches: ['main', 'dev'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const embedTool: Tool = {
|
||||||
|
id: 'embed',
|
||||||
|
button: {
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
branches: ['main', 'dev'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const homeTool: Tool = {
|
||||||
|
id: 'home',
|
||||||
|
button: { color: 'blue' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const galleryTool: Tool = {
|
||||||
|
id: 'gallery',
|
||||||
|
button: {
|
||||||
|
color: 'gold',
|
||||||
|
},
|
||||||
|
branches: ['main', 'dev'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tierListTool: Tool = {
|
||||||
|
id: 'tier-list',
|
||||||
|
stringsId: 'tier_list',
|
||||||
|
button: {
|
||||||
|
color: 'orange',
|
||||||
|
},
|
||||||
|
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chartsTool: Tool = {
|
||||||
|
id: 'charts',
|
||||||
|
button: {
|
||||||
|
color: 'bronze',
|
||||||
|
},
|
||||||
|
branches: ['main', 'dev'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const guessTool: Tool = {
|
||||||
|
id: 'guess',
|
||||||
|
button: { color: 'cyan' },
|
||||||
|
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TOOLS: Tool[] = [
|
||||||
|
tankopediaTool,
|
||||||
|
compareTool,
|
||||||
|
performanceTool,
|
||||||
|
// guessTool,
|
||||||
|
// chartsTool,
|
||||||
|
galleryTool,
|
||||||
|
sessionTool,
|
||||||
|
tierListTool,
|
||||||
|
embedTool,
|
||||||
|
// ratingTool,
|
||||||
|
];
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { assertSecret } from 'packages/core/src';
|
||||||
|
|
||||||
|
export function resolveBranchName(locale: string) {
|
||||||
|
if (assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH) === 'main') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return import.meta.env.PUBLIC_ASSET_BRANCH;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
|
||||||
|
|
||||||
|
export function getBestLocale() {
|
||||||
|
const desiredLocale =
|
||||||
|
localStorage.getItem('preferred-locale') ??
|
||||||
|
navigator.language.split('-')[0];
|
||||||
|
const isSupported = locales.supported.some(
|
||||||
|
({ locale }) => locale === desiredLocale,
|
||||||
|
);
|
||||||
|
const isDefault = desiredLocale === locales.default;
|
||||||
|
|
||||||
|
if (!isSupported || isDefault) return undefined;
|
||||||
|
|
||||||
|
return desiredLocale;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import en from '@strings/en.json' with { type: 'json' };
|
||||||
|
import { merge } from 'lodash-es';
|
||||||
|
import type { BlitzKitStrings } from 'packages/i18n/src';
|
||||||
|
|
||||||
|
export async function getStrings(locale: string) {
|
||||||
|
const strings = await import(`../../../../i18n/strings/${locale}.json`);
|
||||||
|
return merge({}, en, strings) as BlitzKitStrings;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import * as ColorsRaw from '@radix-ui/colors';
|
||||||
|
|
||||||
|
type ExcludeSuffix<T> =
|
||||||
|
T extends `${string}${'A' | `${'P3' | 'Dark'}${'' | 'A'}`}` ? never : T;
|
||||||
|
export type RadixColor = ExcludeSuffix<keyof typeof ColorsRaw>;
|
||||||
|
|
||||||
|
export type VarName =
|
||||||
|
| `color-${'background' | `panel-${'solid' | 'translucent'}` | 'surface' | 'overlay'}`
|
||||||
|
| `radius-${1 | 2 | 3 | 4 | 5 | 6 | 'full'}`
|
||||||
|
| `space-${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`
|
||||||
|
| `shadow-${1 | 2 | 3 | 4 | 5 | 6}`
|
||||||
|
| `${RadixColor | 'accent' | 'gray' | 'black' | 'white'}-${
|
||||||
|
| `${'' | 'a'}${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12}`
|
||||||
|
| 'surface'
|
||||||
|
| 'indicator'
|
||||||
|
| 'track'
|
||||||
|
| 'contrast'}`;
|
||||||
|
|
||||||
|
export function Var(name: VarName) {
|
||||||
|
return `var(--${name})`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { type Draft, produce } from 'immer';
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { type StoreApi, useStore as useStoreZustand } from 'zustand';
|
||||||
|
|
||||||
|
type StoreProviderProps<InitData> = {
|
||||||
|
children: ReactNode;
|
||||||
|
} & (InitData extends unknown ? { data?: InitData } : { data: InitData });
|
||||||
|
|
||||||
|
// idk why zustand doesn't export this
|
||||||
|
type ExtractState<S> = S extends {
|
||||||
|
getState: () => infer T;
|
||||||
|
}
|
||||||
|
? T
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export function createContextualStore<
|
||||||
|
Store extends StoreApi<unknown>,
|
||||||
|
InitData,
|
||||||
|
>(initialize: (data: InitData) => Store) {
|
||||||
|
type Type = Store extends StoreApi<infer T> ? T : never;
|
||||||
|
|
||||||
|
const Context = createContext<Store | undefined>(undefined);
|
||||||
|
|
||||||
|
function Provider(props: StoreProviderProps<InitData>) {
|
||||||
|
const storeRef = useRef<Store | undefined>(undefined);
|
||||||
|
if (!storeRef.current) {
|
||||||
|
storeRef.current = initialize((props as { data: InitData }).data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={storeRef.current}>
|
||||||
|
{props.children}
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useStore() {
|
||||||
|
const store = useContext(Context);
|
||||||
|
if (!store) throw new Error('useStore must be used within Provider');
|
||||||
|
return store as Store;
|
||||||
|
}
|
||||||
|
|
||||||
|
function use<Slice = Type>(
|
||||||
|
selector: (store: Type) => Slice = (state) => state as Slice,
|
||||||
|
) {
|
||||||
|
const store = useStore();
|
||||||
|
return useStoreZustand(
|
||||||
|
store,
|
||||||
|
selector as (state: ExtractState<Store>) => Slice,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDeferred<Slice = Type>(
|
||||||
|
selector: (store: Type) => Slice = (state) => state as Slice,
|
||||||
|
initial: Slice,
|
||||||
|
) {
|
||||||
|
const [state, setState] = useState(initial);
|
||||||
|
const trueState = use(selector);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState(trueState);
|
||||||
|
}, [trueState]);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMutation() {
|
||||||
|
const counterStoreContext = useStore();
|
||||||
|
|
||||||
|
return function (recipe: (draft: Draft<Type>) => void) {
|
||||||
|
counterStoreContext.setState(
|
||||||
|
produce<Type>((draft) => {
|
||||||
|
recipe(draft);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { Provider, use, useMutation, useStore, useDeferred };
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
|
||||||
|
import type { BlitzKitStrings } from 'packages/i18n/src';
|
||||||
|
import { createContext, use, useContext } from 'react';
|
||||||
|
const LocaleContext = createContext<string | null>(null);
|
||||||
|
|
||||||
|
export interface LocaleAcceptorProps {
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocaleProviderProps extends LocaleAcceptorProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocaleProvider({ locale, children }: LocaleProviderProps) {
|
||||||
|
const isSupported = locales.supported.some(
|
||||||
|
(supported) => supported.locale === locale,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isSupported) throw new Error(`Unsupported locale: ${locale}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = new Map<string, Promise<BlitzKitStrings>>();
|
||||||
|
|
||||||
|
function loadLocale(locale: string) {
|
||||||
|
if (cache.has(locale)) return cache.get(locale)!;
|
||||||
|
|
||||||
|
const promise = fetch(`/api/strings/${locale}.json`).then((response) => {
|
||||||
|
if (!response.ok) throw new Error('Failed to load locale');
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.set(locale, promise);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLocale() {
|
||||||
|
const locale = useContext(LocaleContext);
|
||||||
|
|
||||||
|
if (locale === null) {
|
||||||
|
throw new Error('useLocale must be used within a LocaleProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
const strings = use(loadLocale(locale));
|
||||||
|
|
||||||
|
return { locale, strings };
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
import { Flex } from '@radix-ui/themes';
|
||||||
|
import { BlitzKitTheme } from '../components/BlitzKitTheme';
|
||||||
|
import { Checks } from '../components/Checks';
|
||||||
|
import Footer from '../components/Footer.astro';
|
||||||
|
import Navbar from '../components/Navbar.astro';
|
||||||
|
import Party3 from '../components/Party3.astro';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
noFooter?: boolean;
|
||||||
|
noNavbar?: boolean;
|
||||||
|
integrated?: boolean;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { noFooter, noNavbar, integrated, locale } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang={locale}>
|
||||||
|
<head>
|
||||||
|
<meta property="og:site_name" content="BlitzKit" />
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
|
|
||||||
|
<Party3 />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
|
||||||
|
import { getBestLocale } from '../core/i18n/getBestLocale';
|
||||||
|
const isAPI = window.location.pathname.startsWith('/api');
|
||||||
|
const isI18nPath = locales.supported.some(({ locale }) =>
|
||||||
|
window.location.pathname.startsWith(`/${locale}`),
|
||||||
|
);
|
||||||
|
const bestLocale = getBestLocale();
|
||||||
|
const currentPathLocale = locales.supported.find(({ locale }) =>
|
||||||
|
window.location.pathname.startsWith(`/${locale}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isAPI && // not an api, duh!
|
||||||
|
localStorage.getItem('preferred-locale') !== null && // no preference, so probably google's crawler
|
||||||
|
currentPathLocale &&
|
||||||
|
bestLocale !== currentPathLocale.locale // we need to redirect
|
||||||
|
) {
|
||||||
|
window.location.pathname = isI18nPath
|
||||||
|
? window.location.pathname.replace(
|
||||||
|
`/${currentPathLocale.locale}`,
|
||||||
|
bestLocale === undefined ? '' : `/${bestLocale}`,
|
||||||
|
)
|
||||||
|
: `/${bestLocale}${window.location.pathname}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
@import '@radix-ui/themes/styles.css';
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<slot name="head" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class={noNavbar ? 'no-navbar' : ''}>
|
||||||
|
<BlitzKitTheme>
|
||||||
|
<Flex direction="column" minHeight="100vh">
|
||||||
|
<Navbar {locale} {integrated} />
|
||||||
|
|
||||||
|
<Checks {locale} client:only="react" />
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
{!noFooter && <Footer />}
|
||||||
|
</Flex>
|
||||||
|
</BlitzKitTheme>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import locales from '@blitzkit/i18n/locales.json';
|
||||||
|
import { DiscordLogoIcon, HomeIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Button, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||||
|
import { LinkI18n } from '../components/LinkI18n';
|
||||||
|
import { PageWrapper } from '../components/PageWrapper';
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
|
const locale = locales.default;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {locale}>
|
||||||
|
<PageWrapper align="center" justify="center" gap="0">
|
||||||
|
<Heading>404: page not found</Heading>
|
||||||
|
<Text color="gray">Where would you like to go?</Text>
|
||||||
|
|
||||||
|
<Flex mt="4" gap="2">
|
||||||
|
<LinkI18n {locale} href="/">
|
||||||
|
<Button>
|
||||||
|
<HomeIcon /> Home
|
||||||
|
</Button>
|
||||||
|
</LinkI18n>
|
||||||
|
<Link href="https://discord.gg/nDt7AjGJQH">
|
||||||
|
<Button variant="outline">
|
||||||
|
<DiscordLogoIcon /> Discord
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
</PageWrapper>
|
||||||
|
</BaseLayout>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
|
||||||
|
import type { APIRoute, GetStaticPaths, GetStaticPathsItem } from 'astro';
|
||||||
|
import { getStrings } from 'packages/website-ue/src/core/i18n/getStrings';
|
||||||
|
|
||||||
|
export const getStaticPaths: GetStaticPaths = () => {
|
||||||
|
return locales.supported.map(
|
||||||
|
({ locale }) => ({ params: { locale } }) satisfies GetStaticPathsItem,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ params, request }) => {
|
||||||
|
const strings = await getStrings(params.locale!);
|
||||||
|
return Response.json(strings);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const CURRENT_POLICIES_AGREEMENT_INDEX = 0;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { merge } from 'lodash-es';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||||
|
import { createContextualStore } from '../../core/zustand/createContextualStore';
|
||||||
|
|
||||||
|
export interface WargamingLogin {
|
||||||
|
token: string;
|
||||||
|
id: number;
|
||||||
|
expires: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppStore {
|
||||||
|
developerMode: boolean;
|
||||||
|
policiesAgreementIndex: number;
|
||||||
|
logins: {
|
||||||
|
wargaming?: WargamingLogin;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const App = createContextualStore(() =>
|
||||||
|
create<AppStore>()(
|
||||||
|
persist(
|
||||||
|
subscribeWithSelector<AppStore>(() => ({
|
||||||
|
developerMode: false,
|
||||||
|
policiesAgreementIndex: -1,
|
||||||
|
logins: {},
|
||||||
|
})),
|
||||||
|
{ name: 'app', merge: (a, b) => merge(b, a) },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { EmbedConfigItem } from '.';
|
||||||
|
|
||||||
|
export enum EmbedItemType {
|
||||||
|
Radius,
|
||||||
|
Size,
|
||||||
|
SizeWithout0,
|
||||||
|
Color,
|
||||||
|
String,
|
||||||
|
RichText,
|
||||||
|
Boolean,
|
||||||
|
Slider,
|
||||||
|
Enum,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const radixGrays = [
|
||||||
|
'gray',
|
||||||
|
'mauve',
|
||||||
|
'slate',
|
||||||
|
'sage',
|
||||||
|
'olive',
|
||||||
|
'sand',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const radixTextWeights = ['light', 'regular', 'medium', 'bold'] as const;
|
||||||
|
|
||||||
|
export type EmbedConfigItemType<Type extends EmbedItemType> =
|
||||||
|
EmbedConfigItem & { type: Type };
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import * as radixColors from '@radix-ui/colors';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { createContextualStore } from '../../core/zustand/createContextualStore';
|
||||||
|
import {
|
||||||
|
type EmbedConfigItemType,
|
||||||
|
EmbedItemType,
|
||||||
|
radixGrays,
|
||||||
|
radixTextWeights,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
export type RadixSize = `${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;
|
||||||
|
export type RadixSizeWithout0 = Exclude<RadixSize, '0'>;
|
||||||
|
export type RadixRadius = `${1 | 2 | 3 | 4}` | 'full';
|
||||||
|
export type RadixTextWeight = (typeof radixTextWeights)[number];
|
||||||
|
export type RadixColorCompound = {
|
||||||
|
base: RadixColor;
|
||||||
|
variant: RadixColorVariant;
|
||||||
|
};
|
||||||
|
export type RadixTextColor =
|
||||||
|
| Exclude<RadixColor, RadixColorGrays>
|
||||||
|
| 'gray'
|
||||||
|
| undefined;
|
||||||
|
export type RadixColorVariantRaw = `${
|
||||||
|
| 1
|
||||||
|
| 2
|
||||||
|
| 3
|
||||||
|
| 4
|
||||||
|
| 5
|
||||||
|
| 6
|
||||||
|
| 7
|
||||||
|
| 8
|
||||||
|
| 9
|
||||||
|
| 10
|
||||||
|
| 11
|
||||||
|
| 12}`;
|
||||||
|
export type RadixColorVariant = `${'' | 'a'}${RadixColorVariantRaw}`;
|
||||||
|
export type RadixColor = Exclude<
|
||||||
|
keyof typeof radixColors,
|
||||||
|
`${string}${'A' | 'P3' | 'Dark'}`
|
||||||
|
>;
|
||||||
|
export type RadixColorGrays = (typeof radixGrays)[number];
|
||||||
|
|
||||||
|
export type ExtractEmbedConfigTypes<Config extends EmbedConfig> = {
|
||||||
|
[Key in keyof Config]: ({
|
||||||
|
type: Config[Key]['type'];
|
||||||
|
} & EmbedConfigItem)['default'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EmbedConfig = {
|
||||||
|
width: EmbedConfigItemType<EmbedItemType.Slider>;
|
||||||
|
height: EmbedConfigItemType<EmbedItemType.Slider>;
|
||||||
|
} & Record<string, EmbedConfigItem>;
|
||||||
|
|
||||||
|
export type EmbedConfigItem = (
|
||||||
|
| {
|
||||||
|
type: EmbedItemType.Slider;
|
||||||
|
default: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: EmbedItemType.Boolean;
|
||||||
|
default: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: EmbedItemType.String;
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: EmbedItemType.Radius;
|
||||||
|
default: RadixRadius;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: EmbedItemType.Size;
|
||||||
|
default: RadixSize;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: EmbedItemType.SizeWithout0;
|
||||||
|
default: RadixSizeWithout0;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: EmbedItemType.Color;
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: EmbedItemType.RichText;
|
||||||
|
default: {
|
||||||
|
color: string;
|
||||||
|
size: RadixSizeWithout0;
|
||||||
|
weight: RadixTextWeight;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: EmbedItemType.Enum;
|
||||||
|
default: string;
|
||||||
|
options: {
|
||||||
|
value: string;
|
||||||
|
locales: Record<string, string>;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
) & {
|
||||||
|
pad?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EmbedStateStore = Record<string, EmbedConfigItem['default']>;
|
||||||
|
|
||||||
|
export const EmbedState = createContextualStore((init: EmbedStateStore) =>
|
||||||
|
create<EmbedStateStore>()(() => init),
|
||||||
|
);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { TextProps } from '@radix-ui/themes';
|
||||||
|
import { EmbedState, type EmbedConfig, type ExtractEmbedConfigTypes } from '.';
|
||||||
|
import type { EmbedConfigItemType, EmbedItemType } from './constants';
|
||||||
|
|
||||||
|
export function useEmbedStateCurry<Config extends EmbedConfig>() {
|
||||||
|
function useEmbedState<Key extends keyof Config>(key: Key) {
|
||||||
|
return EmbedState.use((state) => {
|
||||||
|
return (state as ExtractEmbedConfigTypes<Config>)[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function useRichText<
|
||||||
|
Key extends {
|
||||||
|
[K in keyof Config]: Config[K] extends { type: EmbedItemType.RichText }
|
||||||
|
? K
|
||||||
|
: never;
|
||||||
|
}[keyof Config],
|
||||||
|
>(key: Key) {
|
||||||
|
const { color, ...state } = useEmbedState(
|
||||||
|
key,
|
||||||
|
) as EmbedConfigItemType<EmbedItemType.RichText>['default'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
style: { color },
|
||||||
|
...state,
|
||||||
|
} satisfies TextProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { useEmbedState, useRichText };
|
||||||
|
}
|
||||||
Reference in new issue
Block a user