mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +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 { SUPPORTED_LOCALES } from '../../../i18n/src/strings';
|
||||
import { I18nString } from '../protos';
|
||||
import { fetchCamouflageDefinitions } from './camouflageDefinitions';
|
||||
import { fetchTankDefinitions } from './tankDefinitions';
|
||||
@@ -26,8 +26,8 @@ export async function fetchTankNames() {
|
||||
searchableNameDeburr,
|
||||
camouflages: tank.camouflages
|
||||
?.map((id) =>
|
||||
SUPPORTED_LOCALES.map(
|
||||
(locale) =>
|
||||
locales.supported.map(
|
||||
({ locale }) =>
|
||||
camouflageDefinitions.camouflages[id]?.name.locales[locale],
|
||||
),
|
||||
)
|
||||
@@ -42,9 +42,11 @@ export async function fetchTankNames() {
|
||||
}
|
||||
|
||||
export const SEARCH_KEYS = [
|
||||
...SUPPORTED_LOCALES.map((locale) => [
|
||||
`searchableName.locales.${locale}`,
|
||||
`searchableNameDeburr.locales.${locale}`,
|
||||
]).flat(),
|
||||
...locales.supported
|
||||
.map(({ locale }) => [
|
||||
`searchableName.locales.${locale}`,
|
||||
`searchableNameDeburr.locales.${locale}`,
|
||||
])
|
||||
.flat(),
|
||||
'camouflages',
|
||||
];
|
||||
@@ -1,89 +1,8 @@
|
||||
import { merge } from 'ts-deepmerge';
|
||||
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';
|
||||
import type en from '../strings/en.json';
|
||||
|
||||
export type BlitzKitStrings = typeof en;
|
||||
|
||||
const stringsRaw = { en, es, fr, ja, pl, pt, ru, uk, zh };
|
||||
|
||||
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> = {
|
||||
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
|
||||
en: '🇺🇸',
|
||||
es: '🇪🇸',
|
||||
fr: '🇫🇷',
|
||||
@@ -95,6 +14,14 @@ export const SUPPORTED_LOCALE_FLAGS: Record<SupportedLocale, string> = {
|
||||
pl: '🇵🇱',
|
||||
};
|
||||
|
||||
export type TranslationTree = { [key: string]: TranslationNode };
|
||||
|
||||
export type TranslationNode = string | TranslationTree;
|
||||
export const LOCALE_NAMES: Record<string, string> = {
|
||||
en: 'English',
|
||||
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 { DEFAULT_LOCALE } from './strings';
|
||||
import locales from '../locales.json' with { type: 'json' };
|
||||
|
||||
export function unwrapper(locale: string) {
|
||||
return function (i18nString: I18nString) {
|
||||
@@ -7,6 +7,6 @@ export function unwrapper(locale: string) {
|
||||
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
|
||||
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