From bc6bb4f10ea2abd8d8afada56a289d332c88f5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 26 May 2025 20:07:33 -0500 Subject: [PATCH] feat: 404 page --- packages/core/src/blitzkit/tankNames.ts | 16 +- packages/i18n/src/strings.ts | 99 +------ packages/i18n/src/unwrapper.ts | 4 +- packages/website-ue/astro.config.mjs | 5 +- .../src/components/BlitzKitTheme.tsx | 30 ++ .../src/components/Checks/index.css | 18 ++ .../src/components/Checks/index.tsx | 157 ++++++++++ .../website-ue/src/components/Footer.astro | 184 ++++++++++++ .../website-ue/src/components/LinkI18n.tsx | 15 + .../src/components/LocaleSwitcher.tsx | 49 ++++ .../website-ue/src/components/Navbar.astro | 270 ++++++++++++++++++ .../website-ue/src/components/PageWrapper.tsx | 54 ++++ .../website-ue/src/components/Party3.astro | 47 +++ packages/website-ue/src/constants/tools.ts | 141 +++++++++ .../website-ue/src/core/blitzkit/branches.ts | 9 + .../website-ue/src/core/i18n/getBestLocale.ts | 15 + .../website-ue/src/core/i18n/getStrings.ts | 8 + packages/website-ue/src/core/radix/var.ts | 21 ++ .../core/zustand/createContextualStore.tsx | 87 ++++++ packages/website-ue/src/hooks/useLocale.tsx | 51 ++++ .../website-ue/src/layouts/BaseLayout.astro | 80 ++++++ packages/website-ue/src/pages/404.astro | 30 ++ .../src/pages/api/strings/[locale].json.ts | 14 + .../website-ue/src/stores/app/constants.ts | 1 + packages/website-ue/src/stores/app/index.ts | 31 ++ .../src/stores/embedState/constants.ts | 27 ++ .../website-ue/src/stores/embedState/index.ts | 109 +++++++ .../src/stores/embedState/utilities.ts | 30 ++ 28 files changed, 1505 insertions(+), 97 deletions(-) create mode 100644 packages/website-ue/src/components/BlitzKitTheme.tsx create mode 100644 packages/website-ue/src/components/Checks/index.css create mode 100644 packages/website-ue/src/components/Checks/index.tsx create mode 100644 packages/website-ue/src/components/Footer.astro create mode 100644 packages/website-ue/src/components/LinkI18n.tsx create mode 100644 packages/website-ue/src/components/LocaleSwitcher.tsx create mode 100644 packages/website-ue/src/components/Navbar.astro create mode 100644 packages/website-ue/src/components/PageWrapper.tsx create mode 100644 packages/website-ue/src/components/Party3.astro create mode 100644 packages/website-ue/src/constants/tools.ts create mode 100644 packages/website-ue/src/core/blitzkit/branches.ts create mode 100644 packages/website-ue/src/core/i18n/getBestLocale.ts create mode 100644 packages/website-ue/src/core/i18n/getStrings.ts create mode 100644 packages/website-ue/src/core/radix/var.ts create mode 100644 packages/website-ue/src/core/zustand/createContextualStore.tsx create mode 100644 packages/website-ue/src/hooks/useLocale.tsx create mode 100644 packages/website-ue/src/layouts/BaseLayout.astro create mode 100644 packages/website-ue/src/pages/404.astro create mode 100644 packages/website-ue/src/pages/api/strings/[locale].json.ts create mode 100644 packages/website-ue/src/stores/app/constants.ts create mode 100644 packages/website-ue/src/stores/app/index.ts create mode 100644 packages/website-ue/src/stores/embedState/constants.ts create mode 100644 packages/website-ue/src/stores/embedState/index.ts create mode 100644 packages/website-ue/src/stores/embedState/utilities.ts diff --git a/packages/core/src/blitzkit/tankNames.ts b/packages/core/src/blitzkit/tankNames.ts index 26dea0712..f33566892 100644 --- a/packages/core/src/blitzkit/tankNames.ts +++ b/packages/core/src/blitzkit/tankNames.ts @@ -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', ]; diff --git a/packages/i18n/src/strings.ts b/packages/i18n/src/strings.ts index 25c022efd..42b23d423 100644 --- a/packages/i18n/src/strings.ts +++ b/packages/i18n/src/strings.ts @@ -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> = {}; - -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 = { - 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 = { +export const SUPPORTED_LOCALE_FLAGS: Record = { en: '🇺🇸', es: '🇪🇸', fr: '🇫🇷', @@ -95,6 +14,14 @@ export const SUPPORTED_LOCALE_FLAGS: Record = { pl: '🇵🇱', }; -export type TranslationTree = { [key: string]: TranslationNode }; - -export type TranslationNode = string | TranslationTree; +export const LOCALE_NAMES: Record = { + en: 'English', + es: 'Español', + fr: 'Français', + ja: '日本語', + pt: 'Português', + ru: 'Русский', + uk: 'Українська', + zh: '中文', + pl: 'Polski', +}; diff --git a/packages/i18n/src/unwrapper.ts b/packages/i18n/src/unwrapper.ts index 7c18ae5ba..9a70fa7a9 100644 --- a/packages/i18n/src/unwrapper.ts +++ b/packages/i18n/src/unwrapper.ts @@ -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]; }; } diff --git a/packages/website-ue/astro.config.mjs b/packages/website-ue/astro.config.mjs index 53e49de9a..cef38998c 100644 --- a/packages/website-ue/astro.config.mjs +++ b/packages/website-ue/astro.config.mjs @@ -5,5 +5,6 @@ import react from '@astrojs/react'; // https://astro.build/config export default defineConfig({ - integrations: [react()] -}); \ No newline at end of file + integrations: [react()], + devToolbar: { enabled: false }, +}); diff --git a/packages/website-ue/src/components/BlitzKitTheme.tsx b/packages/website-ue/src/components/BlitzKitTheme.tsx new file mode 100644 index 000000000..b695358b3 --- /dev/null +++ b/packages/website-ue/src/components/BlitzKitTheme.tsx @@ -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 ( + + {children} + + ); +} diff --git a/packages/website-ue/src/components/Checks/index.css b/packages/website-ue/src/components/Checks/index.css new file mode 100644 index 000000000..ba4682721 --- /dev/null +++ b/packages/website-ue/src/components/Checks/index.css @@ -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; +} diff --git a/packages/website-ue/src/components/Checks/index.tsx b/packages/website-ue/src/components/Checks/index.tsx new file mode 100644 index 000000000..8d6f543e2 --- /dev/null +++ b/packages/website-ue/src/components/Checks/index.tsx @@ -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 ( + + + + + + ); +} + +/** + * 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) + .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 ( + + + + + + {policiesAgreementIndex === -1 + ? "BlitzKit's policies" + : "BlitzKit's policies have changed"} + + + + This website utilizes cookies to perform analytics and personalize + your experience. You can learn more through{' '} + + our privacy policy + + . By using BlitzKit, you also agree to our{' '} + + terms of service + + . + + + + + + + + ); +} diff --git a/packages/website-ue/src/components/Footer.astro b/packages/website-ue/src/components/Footer.astro new file mode 100644 index 000000000..70aed754b --- /dev/null +++ b/packages/website-ue/src/components/Footer.astro @@ -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); +--- + + + + + + + BlitzKit + {strings.website.footer.motto} + + {strings.website.footer.copyright} + + + + + + + + + + {strings.website.footer.about.title} + + + {strings.website.footer.about.credits} + + + {strings.website.footer.about.discord} + + + {strings.website.footer.about.github} + + + {strings.website.footer.about.patches} + + + + + + {strings.website.footer.legal.title} + + + {strings.website.footer.legal.privacy} + + + {strings.website.footer.legal.terms} + + + + + + + + + BlitzKit + { + PUBLIC_ASSET_BRANCH !== 'preview' && ( + + WoTB + + ) + } + CDN + + + + + {packageJson.version} + + { + PUBLIC_ASSET_BRANCH !== 'preview' && ( + + {gameDefinitions.version} + + ) + } + + {PUBLIC_ASSET_BRANCH} + + + + + + BlitzKit + + {packageJson.version} + + + { + PUBLIC_ASSET_BRANCH !== 'preview' && ( + <> + + WoTB + + + {gameDefinitions.version} + + + ) + } + + CDN + + {PUBLIC_ASSET_BRANCH} + + + diff --git a/packages/website-ue/src/components/LinkI18n.tsx b/packages/website-ue/src/components/LinkI18n.tsx new file mode 100644 index 000000000..9c38e3c34 --- /dev/null +++ b/packages/website-ue/src/components/LinkI18n.tsx @@ -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 ( + + ); +} diff --git a/packages/website-ue/src/components/LocaleSwitcher.tsx b/packages/website-ue/src/components/LocaleSwitcher.tsx new file mode 100644 index 000000000..80850c4fd --- /dev/null +++ b/packages/website-ue/src/components/LocaleSwitcher.tsx @@ -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 ( + + + + ); +} + +export function LocaleSwitcher({ locale }: LocaleAcceptorProps) { + return ( + { + 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}`; + }} + > + + + + {locales.supported.map(({ locale }) => ( + + {SUPPORTED_LOCALE_FLAGS[locale]} {LOCALE_NAMES[locale]} + + ))} + + + ); +} diff --git a/packages/website-ue/src/components/Navbar.astro b/packages/website-ue/src/components/Navbar.astro new file mode 100644 index 000000000..2f6789242 --- /dev/null +++ b/packages/website-ue/src/components/Navbar.astro @@ -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); +--- + + + + + +{ + !integrated && ( + + ) +} + + + + + + + + BlitzKit + { + isBranchNamed && ( + + {branchName} + + ) + } + + + + + + + { + 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 ( + + {toolStrings.name} + + ); + }) + } + + + + + + + + + + + + + + + + + + + + + + + + + + { + 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 ( + + + + {toolStrings.name} + + + + ); + }) + } + + + + + + diff --git a/packages/website-ue/src/components/PageWrapper.tsx b/packages/website-ue/src/components/PageWrapper.tsx new file mode 100644 index 000000000..7ec4e7101 --- /dev/null +++ b/packages/website-ue/src/components/PageWrapper.tsx @@ -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['accentColor']; + containerProps?: ComponentProps; + noFlex1?: boolean; + padding?: RadixSize; + noMinHeight?: boolean; +}; + +export function PageWrapper({ + style, + color, + children, + noFlex1 = false, + containerProps, + noMinHeight = false, + padding = '4', + ...props +}: PageWrapperProps) { + return ( + + + {children} + + + ); +} diff --git a/packages/website-ue/src/components/Party3.astro b/packages/website-ue/src/components/Party3.astro new file mode 100644 index 000000000..415e5b90d --- /dev/null +++ b/packages/website-ue/src/components/Party3.astro @@ -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 */} + + + +{/* nitro ads */} +{ + /* + +*/ +} diff --git a/packages/website-ue/src/constants/tools.ts b/packages/website-ue/src/constants/tools.ts new file mode 100644 index 000000000..b7b548ae0 --- /dev/null +++ b/packages/website-ue/src/constants/tools.ts @@ -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['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, +]; diff --git a/packages/website-ue/src/core/blitzkit/branches.ts b/packages/website-ue/src/core/blitzkit/branches.ts new file mode 100644 index 000000000..841d7ea5e --- /dev/null +++ b/packages/website-ue/src/core/blitzkit/branches.ts @@ -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; +} diff --git a/packages/website-ue/src/core/i18n/getBestLocale.ts b/packages/website-ue/src/core/i18n/getBestLocale.ts new file mode 100644 index 000000000..1b80b711e --- /dev/null +++ b/packages/website-ue/src/core/i18n/getBestLocale.ts @@ -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; +} diff --git a/packages/website-ue/src/core/i18n/getStrings.ts b/packages/website-ue/src/core/i18n/getStrings.ts new file mode 100644 index 000000000..af0dfde56 --- /dev/null +++ b/packages/website-ue/src/core/i18n/getStrings.ts @@ -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; +} diff --git a/packages/website-ue/src/core/radix/var.ts b/packages/website-ue/src/core/radix/var.ts new file mode 100644 index 000000000..29b986ba8 --- /dev/null +++ b/packages/website-ue/src/core/radix/var.ts @@ -0,0 +1,21 @@ +import * as ColorsRaw from '@radix-ui/colors'; + +type ExcludeSuffix = + T extends `${string}${'A' | `${'P3' | 'Dark'}${'' | 'A'}`}` ? never : T; +export type RadixColor = ExcludeSuffix; + +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})`; +} diff --git a/packages/website-ue/src/core/zustand/createContextualStore.tsx b/packages/website-ue/src/core/zustand/createContextualStore.tsx new file mode 100644 index 000000000..58843fa0e --- /dev/null +++ b/packages/website-ue/src/core/zustand/createContextualStore.tsx @@ -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 = { + children: ReactNode; +} & (InitData extends unknown ? { data?: InitData } : { data: InitData }); + +// idk why zustand doesn't export this +type ExtractState = S extends { + getState: () => infer T; +} + ? T + : never; + +export function createContextualStore< + Store extends StoreApi, + InitData, +>(initialize: (data: InitData) => Store) { + type Type = Store extends StoreApi ? T : never; + + const Context = createContext(undefined); + + function Provider(props: StoreProviderProps) { + const storeRef = useRef(undefined); + if (!storeRef.current) { + storeRef.current = initialize((props as { data: InitData }).data); + } + + return ( + + {props.children} + + ); + } + + function useStore() { + const store = useContext(Context); + if (!store) throw new Error('useStore must be used within Provider'); + return store as Store; + } + + function use( + selector: (store: Type) => Slice = (state) => state as Slice, + ) { + const store = useStore(); + return useStoreZustand( + store, + selector as (state: ExtractState) => Slice, + ); + } + + function useDeferred( + 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) => void) { + counterStoreContext.setState( + produce((draft) => { + recipe(draft); + }), + ); + }; + } + + return { Provider, use, useMutation, useStore, useDeferred }; +} diff --git a/packages/website-ue/src/hooks/useLocale.tsx b/packages/website-ue/src/hooks/useLocale.tsx new file mode 100644 index 000000000..5420af749 --- /dev/null +++ b/packages/website-ue/src/hooks/useLocale.tsx @@ -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(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 ( + {children} + ); +} + +const cache = new Map>(); + +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 }; +} diff --git a/packages/website-ue/src/layouts/BaseLayout.astro b/packages/website-ue/src/layouts/BaseLayout.astro new file mode 100644 index 000000000..aaebf04f9 --- /dev/null +++ b/packages/website-ue/src/layouts/BaseLayout.astro @@ -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; +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + {!noFooter &&