Compare commits

...
5 Commits
Author SHA1 Message Date
TrèsAbhi 323d624ec7 feat: idk bro 2026-01-18 18:38:26 -06:00
TrèsAbhi 49f84db473 feat: recently viewed 2026-01-18 17:55:38 -06:00
TrèsAbhi 7399c402af feat: ahhhhhhhhhh 2026-01-18 15:14:26 -06:00
TrèsAbhi cbfcb29691 feat: new navbar 2026-01-18 14:51:55 -06:00
TrèsAbhi ae616fbaf3 feat: cleaner popup 2026-01-18 14:12:20 -06:00
26 changed files with 448 additions and 99 deletions

No files matched your search

+51 -2
View File
@@ -40,17 +40,56 @@
"name": "API",
"action": "To docs",
"slogan": "BlitzKit hosted API"
},
"settings": {
"name": "Settings"
},
"players": {
"name": "Players",
"slogan": "Player statistics",
"action": "Stalk player"
},
"compare": {
"name": "Compare",
"slogan": "Compare tanks characteristics",
"action": "Compare now"
},
"performance": {
"name": "Performance",
"slogan": "Tank performance statistics",
"action": "View tanks"
},
"playlist": {
"name": "Playlist",
"slogan": "Tank playlist generator",
"action": "Generate"
},
"mixer": {
"name": "Mixer",
"slogan": "Tank module mixer",
"action": "Mix and match"
},
"session": {
"name": "Session",
"slogan": "Live player statistics",
"action": "Track"
},
"guesser": {
"name": "Guesser",
"slogan": "Silhouette mini-game",
"action": "Play"
}
},
"home": {
"search": "Search tanks, players, tools...",
"search": "Search tanks, players, avatars...",
"credit": "by TrèsAbhi"
},
"avatars": {
"search": "Search avatars...",
"no_description": "Avatar has no description."
"no_description": "Avatar has no description.",
"download": "Download"
},
"grades": {
@@ -59,5 +98,15 @@
"2": "Rare",
"3": "Epic",
"4": "Legendary"
},
"navbar": {
"home": "Home",
"settings": "Settings"
},
"recent": {
"title": "Recent",
"no_recent": "Your recently viewed pages will appear here."
}
}
@@ -50,7 +50,7 @@
.details {
display: flex;
gap: var(--space-4);
gap: var(--space-5);
align-items: center;
.preview {
@@ -1,14 +1,16 @@
import { literals } from "@blitzkit/i18n";
import { Grade } from "@protos/auto_items";
import { StarFilledIcon } from "@radix-ui/react-icons";
import { DownloadIcon, StarFilledIcon } from "@radix-ui/react-icons";
import type { Color } from "../../core/ui/color";
import { useGameStrings } from "../../hooks/useGameStrings";
import { useStrings } from "../../hooks/useStrings";
import type { Avatar } from "../../protos/avatar";
import type { PropsWithSkeleton } from "../../types/propsWithSkeleton";
import { Badge } from "../Badge";
import { Button } from "../Button";
import { Dialog } from "../Dialog";
import { InlineSkeleton } from "../InlineSkeleton";
import { Link } from "../Link";
import { Skeleton } from "../Skeleton";
import { Text } from "../Text";
import styles from "./index.module.css";
@@ -97,6 +99,16 @@ function Details({ avatars }: DetailsProps) {
strings.avatars.no_description
)}
</Text>
<Link
underline="never"
href={`/api/avatars/${avatar.name}.webp`}
download
>
<Button>
<DownloadIcon /> {strings.avatars.download}
</Button>
</Link>
</div>
</div>
);
@@ -2,6 +2,7 @@
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
border-radius: var(--max-size);
padding: var(--space-3) var(--space-4);
@@ -1,7 +1,7 @@
import type { ComponentProps } from "react";
import { classNames } from "../../core/ui/classNames";
import type { Color } from "../../core/ui/color";
import "./index.css";
import styles from "./index.module.css";
export interface ButtonProps extends ComponentProps<"button"> {
color?: Color;
@@ -25,7 +25,7 @@ export function Button({
}: ButtonProps) {
return (
<button
className={classNames("button", className)}
className={classNames(styles.button, className)}
data-color={color}
data-variant={variant}
data-light-text={color ? LIGHT_TEXT_COLORS.has(color) : undefined}
@@ -9,6 +9,7 @@
left: 0;
width: 100vw;
height: 100dvh;
overflow-y: scroll;
background-color: var(--black-a9);
z-index: 1;
@@ -16,12 +17,14 @@
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-4);
box-sizing: border-box;
.content {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin: var(--space-4);
box-sizing: border-box;
min-width: 25rem;
@@ -31,7 +31,7 @@ const strings = await api.strings(locale);
.logo {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
.icon {
width: var(--space-6);
@@ -41,7 +41,6 @@ const strings = await api.strings(locale);
.info {
display: flex;
flex-direction: column;
align-items: end;
gap: var(--space-1);
}
}
@@ -31,8 +31,6 @@ const strings = await api.strings(locale);
width: 100%;
height: calc(100% - var(--space-5) / 2);
z-index: -1;
background-image: url(/assets/home-hero.webp);
background-size: cover;
background-position: top;
@@ -81,9 +79,10 @@ const strings = await api.strings(locale);
}
}
.text-field {
.search {
width: 100%;
max-width: 25rem;
z-index: 0;
}
}
</style>
@@ -104,7 +103,7 @@ const strings = await api.strings(locale);
</Text>
</div>
<TextField placeholder={strings.home.search}>
<TextField className="search" placeholder={strings.home.search}>
<MagnifyingGlassIcon />
</TextField>
</div>
@@ -11,4 +11,13 @@
&[data-underline="hover"]:focus {
text-decoration: underline;
}
&[data-high-contrast="true"] {
color: var(--accent-12);
}
svg {
width: 1.25em;
height: 1.25em;
}
}
@@ -4,14 +4,21 @@ import "./index.css";
export interface LinkProps extends ComponentProps<"a"> {
underline?: "always" | "hover" | "never";
highContrast?: boolean;
}
export function Link({ className, underline = "always", ...props }: LinkProps) {
export function Link({
highContrast,
className,
underline = "always",
...props
}: LinkProps) {
return (
<a
className={classNames("link", className)}
{...props}
data-underline={underline}
data-high-contrast={highContrast}
/>
);
}
+107 -34
View File
@@ -1,63 +1,113 @@
---
import { HamburgerMenuIcon, MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { BlitzKitIcon } from "../icons/BlitzKit";
import { LinkI18n } from "./LinkI18n";
import { Text } from "./Text";
import { api } from "../core/api/dynamic";
import { tools } from "../core/tools/tools";
import type { Tool } from "../core/tools/tools";
interface Props {
locale: string;
selectedTool?: Tool;
}
const { locale } = Astro.props;
const { locale, selectedTool } = Astro.props;
const strings = await api.strings(locale);
const toolsArray = Object.values(tools);
const toolsSorted =
selectedTool === undefined
? toolsArray
: [selectedTool, ...toolsArray.filter((tool) => tool !== selectedTool)];
---
<style>
#navbar {
position: sticky;
top: 0;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
z-index: 1;
backdrop-filter: var(--blur-glass) brightness(25%);
background-color: var(--accent-2);
background-color: var(--gray-a5);
box-shadow: var(--shadow-none);
backdrop-filter: var(--blur-glass) brightness(5%);
border-radius: var(--space-4) var(--space-4) 0 0;
transition-duration: var(--duration-brief);
&[data-sticky="true"] {
background-color: var(--gray-a2);
box-shadow: var(--shadow-subtle);
transform: translateY(100%);
}
.bar {
width: 100%;
max-width: var(--space-6);
height: var(--space-2);
margin-top: var(--space-3);
background-color: var(--white-a2);
border-radius: var(--max-size);
}
.content {
flex: 1;
width: 100%;
max-width: calc(var(--page-width) + 2 * var(--space-4));
display: flex;
align-items: center;
justify-content: space-between;
display: grid;
grid-template-columns: repeat(4, 1fr);
.action {
flex: 1;
.side {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-3);
gap: var(--space-2);
.action {
padding: var(--space-4);
cursor: pointer;
color: var(--color-regular);
text-decoration: none;
color: var(--gray-11);
cursor: pointer;
color: var(--color-regular);
&[data-selected="true"] {
color: var(--accent-12);
&:focus {
outline: var(--outline-focus);
.selector {
background-color: var(--accent-5);
}
.label {
color: var(--gray-12) !important;
}
}
.selector {
padding: var(--space-2);
border-radius: var(--max-size);
display: flex;
justify-content: center;
width: 100%;
max-width: var(--space-6);
.icon {
width: 1.25em;
height: 1.25em;
width: 100%;
vertical-align: top;
}
}
.label {
font-size: var(--font-size-minor);
}
}
}
}
@@ -65,9 +115,12 @@ const { locale } = Astro.props;
<script>
const navbar = document.getElementById("navbar")!;
let lastScrollY = window.scrollY;
function handleScroll() {
navbar.dataset.sticky = `${window.scrollY > 0}`;
const dy = window.scrollY - lastScrollY;
lastScrollY = window.scrollY;
navbar.dataset.sticky = `${dy > 0}`;
}
handleScroll();
@@ -75,21 +128,41 @@ const { locale } = Astro.props;
</script>
<div id="navbar">
<div class="bar"></div>
<div class="content">
<div class="side">
<LinkI18n {locale} href="/" className="action" tabIndex={0}>
<LinkI18n
{locale}
href="/"
className="action"
tabIndex={0}
data-selected={selectedTool === undefined}
>
<div class="selector">
<BlitzKitIcon className="icon" />
</LinkI18n>
</div>
<div class="side">
<div class="action" tabindex="0">
<MagnifyingGlassIcon className="icon" />
</div>
<Text lowContrast className="label">{strings.navbar.home}</Text>
</LinkI18n>
<div class="action" tabindex="0">
<HamburgerMenuIcon className="icon" />
</div>
</div>
{
toolsSorted.map(({ id, Icon }) => (
<LinkI18n
key={id}
{locale}
href={`/${id}`}
className="action"
tabIndex={0}
data-selected={selectedTool?.id === id}
>
<div class="selector">
<Icon className="icon" />
</div>
<Text lowContrast className="label">
{strings.tools[id as keyof typeof strings.tools].name}
</Text>
</LinkI18n>
))
}
</div>
</div>
@@ -0,0 +1,9 @@
.recent {
.list {
min-height: var(--space-7);
display: flex;
align-items: center;
justify-content: center;
}
}
@@ -0,0 +1,34 @@
import { LocaleProvider } from "../../hooks/useLocale";
import { useStrings } from "../../hooks/useStrings";
import { Heading } from "../Heading";
import { Section } from "../Section";
import { Text } from "../Text";
import styles from "./index.module.css";
interface Props {
locale: string;
skeleton?: boolean;
}
export function RecentItems({ locale, skeleton }: Props) {
return (
<LocaleProvider locale={locale}>
<Content />
</LocaleProvider>
);
}
function Content() {
const strings = useStrings();
return (
<Section first className={styles.recent}>
<Heading size="3">{strings.recent.title}</Heading>
<div className={styles.list}>
<Text lowContrast size="minor">
{strings.recent.no_recent}
</Text>
</div>
</Section>
);
}
@@ -1,7 +1,7 @@
.section {
display: flex;
justify-content: center;
padding: 0 var(--space-4);
padding: var(--space-4);
.content {
flex: 1;
@@ -11,4 +11,8 @@
flex-direction: column;
gap: var(--space-4);
}
&[data-first="true"] .content {
padding-top: var(--space-6);
}
}
@@ -2,13 +2,17 @@ import type { ComponentProps } from "react";
import { classNames } from "../../core/ui/classNames";
import "./index.css";
export function Section({
className,
children,
...props
}: ComponentProps<"div">) {
export interface SectionProps extends ComponentProps<"div"> {
first?: boolean;
}
export function Section({ className, children, ...props }: SectionProps) {
return (
<div className={classNames("section", className)} {...props}>
<div
data-first={props.first}
className={classNames("section", className)}
{...props}
>
<div className="content">{children}</div>
</div>
);
@@ -13,19 +13,20 @@ export interface TextFieldProps extends ComponentProps<"input"> {
}
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
({ children, className, disabled, ...props }, ref) => {
({ children, className, placeholder, disabled, ...props }, ref) => {
const input = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => input.current!, []);
return (
<div
{...props}
className={classNames("text-field", className)}
data-has-icon={children !== undefined}
data-disabled={disabled}
>
{children}
<input disabled={disabled} ref={input} {...props} />
<input disabled={disabled} ref={input} placeholder={placeholder} />
</div>
);
}
@@ -1,7 +1,9 @@
---
import { ArrowLeftIcon } from "@radix-ui/react-icons";
import { api } from "../core/api/dynamic";
import type { Tool } from "../core/tools/tools";
import { Heading } from "./Heading";
import { LinkI18n } from "./LinkI18n";
import { Text } from "./Text";
interface Props {
@@ -17,7 +19,6 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
<style>
.tool-banner {
background-size: cover;
margin-bottom: calc(var(--space-5) - var(--space-4));
.content {
width: 100%;
@@ -25,7 +26,8 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
position: relative;
display: flex;
justify-content: center;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-5) var(--space-4);
box-sizing: border-box;
@@ -35,6 +37,10 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
padding: var(--space-6) var(--space-4);
}
.back {
z-index: 1;
}
.centered {
flex: 1;
max-width: var(--page-width);
@@ -71,11 +77,13 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
}}
>
<div class="content">
<LinkI18n highContrast className="back" {locale} href="/">
<ArrowLeftIcon />
</LinkI18n>
<div
class="pattern"
style={{
backgroundImage: `url(/assets/patterns/${tool.id}.svg)`,
}}
style={{ backgroundImage: `url(/assets/patterns/${tool.id}.svg)` }}
>
</div>
+86 -14
View File
@@ -1,46 +1,118 @@
import type { ButtonProps } from "@radix-ui/themes";
import {
ExclamationTriangleIcon,
GearIcon,
PersonIcon,
} from "@radix-ui/react-icons";
import type { IconProps } from "@radix-ui/react-icons/dist/types";
import type { ComponentProps, ReactNode } from "react";
import { TankIcon } from "../../icons/Tank";
import type { Color } from "../ui/color";
export interface Tool {
id: string;
strings?: string;
button: ButtonProps["color"];
significant?: boolean;
disabled?: boolean;
href?: string;
accent: Color;
banner_background_position: "top" | "center" | "bottom";
Icon:
| ((props: ComponentProps<"svg">) => ReactNode)
| ((props: IconProps) => ReactNode);
}
export const tools: Record<string, Tool> = {
tanks: {
id: "tanks",
strings: "tankopedia",
button: "purple",
significant: true,
Icon: TankIcon,
accent: "purple",
banner_background_position: "top",
},
players: {
id: "players",
Icon: PersonIcon,
accent: "purple",
banner_background_position: "top",
},
settings: {
id: "settings",
Icon: GearIcon,
accent: "gray",
banner_background_position: "top",
},
avatars: {
id: "avatars",
button: "gold",
Icon: ExclamationTriangleIcon,
accent: "gold",
banner_background_position: "bottom",
},
backgrounds: {
id: "backgrounds",
button: "gold",
Icon: ExclamationTriangleIcon,
accent: "gold",
banner_background_position: "bottom",
},
api: {
id: "api",
disabled: true,
button: "lime",
Icon: ExclamationTriangleIcon,
accent: "lime",
banner_background_position: "bottom",
},
compare: {
id: "compare",
Icon: ExclamationTriangleIcon,
accent: "crimson",
banner_background_position: "bottom",
},
performance: {
id: "performance",
Icon: ExclamationTriangleIcon,
accent: "jade",
banner_background_position: "bottom",
},
playlist: {
id: "playlist",
Icon: ExclamationTriangleIcon,
accent: "tomato",
banner_background_position: "bottom",
},
mixer: {
id: "mixer",
Icon: ExclamationTriangleIcon,
accent: "gray",
banner_background_position: "bottom",
},
guesser: {
id: "guesser",
Icon: ExclamationTriangleIcon,
accent: "cyan",
banner_background_position: "bottom",
},
session: {
id: "session",
Icon: ExclamationTriangleIcon,
accent: "blue",
banner_background_position: "bottom",
},
};
+20
View File
@@ -0,0 +1,20 @@
import type { ComponentProps } from "react";
export function TankIcon(props: ComponentProps<"svg">) {
return (
<svg
width="64"
height="48"
viewBox="0 0 64 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M63 14.3344H42.7572M58.2615 31.6656C58.2615 37.0758 53.8954 38 48.5095 38H11.752C6.36612 38 2 37.0758 2 31.6656C2 26.2555 6.36612 21.8697 11.752 21.8697H23.8795V17.7959C23.8795 12.3858 28.2456 8 33.6315 8C39.0174 8 43.3835 12.3858 43.3835 17.7959V21.8697H48.5095C53.8954 21.8697 58.2615 26.2555 58.2615 31.6656Z"
stroke="currentColor"
stroke-width="4"
/>
</svg>
);
}
@@ -3,17 +3,18 @@ import BaseMeta from "../components/BaseMeta.astro";
import BaseStyles from "../components/BaseStyles.astro";
import ColorAlias from "../components/ColorAlias.astro";
import ColorScale from "../components/ColorScale.astro";
import Footer from "../components/Footer.astro";
import Navbar from "../components/Navbar.astro";
import type { Tool } from "../core/tools/tools";
import type { Color } from "../core/ui/color";
interface Props {
locale: string;
accent: Color;
noNavbar?: boolean;
selectedTool?: Tool;
}
const { accent, noNavbar, locale } = Astro.props;
const { accent, noNavbar, locale, selectedTool } = Astro.props;
---
<html lang={locale}>
@@ -42,7 +43,7 @@ const { accent, noNavbar, locale } = Astro.props;
flex-direction: column;
min-height: 100dvh;
background-color: var(--gray-1);
background-color: var(--gray-3);
color: var(--gray-12);
font-family: var(--font-family-regular);
@@ -50,8 +51,8 @@ const { accent, noNavbar, locale } = Astro.props;
display: flex;
flex: 1;
flex-direction: column;
gap: var(--space-5);
padding: var(--space-4) 0;
background-color: var(--gray-1);
}
}
</style>
@@ -60,14 +61,12 @@ const { accent, noNavbar, locale } = Astro.props;
</head>
<body class="dark">
{!noNavbar && <Navbar {locale} />}
<slot name="pre-app" />
<div class="app">
<slot />
</div>
<Footer {locale} />
{!noNavbar && <Navbar {selectedTool} {locale} />}
</body>
</html>
@@ -0,0 +1,34 @@
---
import locales from "@blitzkit/i18n/locales.json";
import "@radix-ui/colors/amber-dark.css";
import type { GetStaticPaths } from "astro";
import ColorScale from "../../components/ColorScale.astro";
import HomeHero from "../../components/HomeHero.astro";
import { Section } from "../../components/Section";
import ToolCard from "../../components/ToolCard.astro";
import { tools } from "../../core/tools/tools";
import BaseLayout from "../../layouts/BaseLayout.astro";
export const getStaticPaths = (() => {
return [
...locales.supported
.filter(({ locale }) => locale !== locales.default)
.map(({ locale }) => ({ params: { locale }, props: { locale } })),
{ params: { locale: undefined }, props: { locale: locales.default } },
];
}) satisfies GetStaticPaths;
const { locale } = Astro.props;
---
<BaseLayout {locale} accent="amber" noNavbar>
<ColorScale color="purple" slot="head" />
<ColorScale color="gold" slot="head" />
<ColorScale color="lime" slot="head" />
<HomeHero {locale} />
<Section>
{Object.values(tools).map((tool) => <ToolCard {locale} {tool} />)}
</Section>
</BaseLayout>
@@ -1,6 +1,7 @@
---
import "@radix-ui/colors/amber-dark.css";
import { getCollection } from "astro:content";
import { tools } from "../../../core/tools/tools";
import BaseLayout from "../../../layouts/BaseLayout.astro";
export { getStaticPaths } from "../index.astro";
@@ -18,7 +19,7 @@ const paths: {
paths.sort((a, b) => a.path.localeCompare(b.path));
---
<BaseLayout {locale} accent="lime">
<BaseLayout selectedTool={tools.api} {locale} accent="lime">
<h1>api</h1>
{
@@ -10,7 +10,7 @@ export { getStaticPaths } from "../index.astro";
const { locale } = Astro.props;
---
<BaseLayout {locale} accent="gold">
<BaseLayout selectedTool={tools.avatars} {locale} accent="gold">
<ColorScale color="purple" />
<ColorScale color="blue" />
@@ -1,12 +1,8 @@
---
import locales from "@blitzkit/i18n/locales.json";
import "@radix-ui/colors/amber-dark.css";
import type { GetStaticPaths } from "astro";
import ColorScale from "../../components/ColorScale.astro";
import HomeHero from "../../components/HomeHero.astro";
import { Section } from "../../components/Section";
import ToolCard from "../../components/ToolCard.astro";
import { tools } from "../../core/tools/tools";
import { RecentItems } from "../../components/RecentItems";
import BaseLayout from "../../layouts/BaseLayout.astro";
export const getStaticPaths = (() => {
@@ -21,14 +17,14 @@ export const getStaticPaths = (() => {
const { locale } = Astro.props;
---
<BaseLayout {locale} accent="amber" noNavbar>
<ColorScale color="purple" slot="head" />
<ColorScale color="gold" slot="head" />
<ColorScale color="lime" slot="head" />
<BaseLayout {locale} accent="amber">
<HomeHero {locale} />
<Section>
<RecentItems {locale} client:only="react">
<RecentItems {locale} skeleton slot="fallback" />
</RecentItems>
<!-- <Section>
{Object.values(tools).map((tool) => <ToolCard {locale} {tool} />)}
</Section>
</Section> -->
</BaseLayout>
@@ -0,0 +1,15 @@
---
import { Heading } from "../../../components/Heading";
import { Section } from "../../../components/Section";
import BaseLayout from "../../../layouts/BaseLayout.astro";
export { getStaticPaths } from "../index.astro";
const { locale } = Astro.props;
---
<BaseLayout {locale} accent="amber">
<Section>
<Heading>Settings</Heading>
</Section>
</BaseLayout>
@@ -11,7 +11,7 @@ const { locale } = Astro.props;
const tankList = await api.tankList();
---
<BaseLayout {locale} accent="purple">
<BaseLayout selectedTool={tools.tanks} {locale} accent="purple">
<ToolBanner {locale} tool={tools.tanks} slot="pre-app" />
<h1>tanks</h1>