Compare commits

..
6 Commits
Author SHA1 Message Date
TrèsAbhi ea412086d4 feat: start of avatars 2026-01-12 14:13:21 -06:00
TrèsAbhi 455679a981 feat: some footer 2026-01-12 14:00:13 -06:00
TrèsAbhi a21bc6174d feat: some navbar 2026-01-12 13:46:58 -06:00
TrèsAbhi fae1a89402 feat: some more apis 2026-01-12 13:20:08 -06:00
TrèsAbhi 5a1a0c59f1 feat: profile and backgroud apis 2026-01-12 13:12:11 -06:00
TrèsAbhi c98271f783 feat: darker input 2026-01-12 12:48:47 -06:00
28 changed files with 316 additions and 109 deletions

No files matched your search

@@ -0,0 +1,19 @@
.avatar-card {
display: flex;
flex-direction: column;
align-items: center;
.avatar-image {
width: 100%;
aspect-ratio: 7 / 8;
background-size: cover;
background-position: center;
}
.avatar-label {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
}
@@ -0,0 +1,19 @@
import type { Avatar } from "../../protos/avatar";
import { Text } from "../Text";
import "./index.css";
interface Props {
avatar: Avatar;
}
export function AvatarCard({ avatar }: Props) {
return (
<div className="avatar-card">
<div
className="avatar-image"
style={{ backgroundImage: `url(/api/avatars/${avatar.name}.webp)` }}
/>
<Text className="avatar-label">{avatar.name}</Text>
</div>
);
}
@@ -20,10 +20,14 @@ import "@fontsource-variable/roboto-flex";
--font-size-regular: 1rem;
--font-size-minor: 0.75rem;
--font-size-major: 1.25rem;
--font-family-regular: "Roboto Flex Variable", sans-serif;
--font-family-mono: "JetBrains Mono Variable", monospace;
--color-regular: var(--gray-12);
--color-low-contrast: var(--gray-11);
--weight-light: 300;
--weight-regular: 400;
--weight-medium: 500;
@@ -43,5 +47,7 @@ import "@fontsource-variable/roboto-flex";
var(--space-1) var(--white-a3),
inset calc(var(--space-1) * sin(45deg) * -1)
calc(var(--space-1) * sin(45deg) * -1) var(--space-1) var(--black-a3);
--blur-glass: blur(4rem);
}
</style>
@@ -1,12 +1,13 @@
---
import type { Color } from "../pages/styles/colors/[accent].css";
import type { Color } from "../core/ui/color";
interface Props {
color: Color;
alias: string;
alpha?: boolean;
}
const { color, alias } = Astro.props;
const { color, alias, alpha } = Astro.props;
let styles = "";
@@ -14,7 +15,7 @@ styles += ":root{";
for (let i = 0; i < 12; i++) {
const n = i + 1;
styles += `--${alias}-${n}:var(--${color}-${n});`;
styles += `--${alias}-${alpha ? "a" : ""}${n}:var(--${color}-${alpha ? "a" : ""}${n});`;
}
styles += "}";
@@ -1,20 +1,75 @@
---
import packageJson from "../../../../package.json";
import { api } from "../core/api/dynamic";
import { BlitzKitIcon } from "../icons/BlitzKit";
import { Code } from "./Code";
import { Text } from "./Text";
interface Props {
locale: string;
}
const { locale } = Astro.props;
const strings = await api.strings(locale);
---
<style>
.footer {
display: flex;
justify-content: center;
margin-top: calc(var(--space-4) * -1);
padding: var(--space-6) var(--space-4);
padding: var(--space-5) var(--space-5);
background-color: var(--gray-3);
color: var(--color-low-contrast);
.content {
flex: 1;
max-width: 52rem;
.logo {
display: flex;
align-items: center;
justify-content: space-between;
.icon {
width: var(--space-6);
height: var(--space-6);
}
.info {
display: flex;
flex-direction: column;
align-items: end;
gap: var(--space-1);
}
}
}
}
.footer-spacer {
flex: 1;
}
</style>
<div class="footer-spacer"></div>
<div class="footer">
<div class="content">poopy</div>
<div class="content">
<div class="logo">
<BlitzKitIcon className="icon" />
<div class="info">
<Text lowContrast weight="medium" size="major">
{strings.common.name}
</Text>
<Code>
{packageJson.version}
</Code>
</div>
</div>
</div>
</div>
@@ -0,0 +1,56 @@
---
import { HamburgerMenuIcon, MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { BlitzKitIcon } from "../icons/BlitzKit";
---
<style>
.navbar {
position: sticky;
top: 0;
width: 100%;
margin-bottom: calc(var(--space-5) * -1);
display: flex;
justify-content: space-between;
z-index: 1;
backdrop-filter: var(--blur-glass);
background-color: var(--black-a11);
.side {
display: flex;
.action {
width: calc(var(--space-5) + var(--space-4));
height: calc(var(--space-5) + var(--space-4));
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 1.25em;
height: 1.25em;
}
}
}
}
</style>
<div class="navbar">
<div class="side">
<div class="action">
<BlitzKitIcon className="icon" />
</div>
</div>
<div class="side">
<div class="action">
<MagnifyingGlassIcon className="icon" />
</div>
<div class="action">
<HamburgerMenuIcon className="icon" />
</div>
</div>
</div>
@@ -5,6 +5,10 @@
font-size: var(--font-size-minor);
}
&[data-size="major"] {
font-size: var(--font-size-major);
}
&[data-weight="light"] {
font-weight: var(--weight-light);
}
@@ -7,7 +7,7 @@ export interface TextProps extends ComponentProps<"span"> {
color?: Color;
weight?: "light" | "regular" | "medium" | "bold" | "black";
lowContrast?: boolean;
size?: "regular" | "minor";
size?: "regular" | "minor" | "major";
}
export function Text({
@@ -8,7 +8,7 @@
border-radius: var(--max-size);
background-color: var(--gray-3);
background-color: var(--gray-1);
outline: var(--outline-interactive);
box-shadow: var(--shadow-cavity);
+3 -13
View File
@@ -1,19 +1,9 @@
import { fetchPB } from "@blitzkit/core";
import { Tank } from "../../protos/tank";
import { TankList } from "../../protos/tank_list";
import { Tanks } from "../../protos/tanks";
import { Avatars } from "../../protos/avatars";
import { AbstractAPI } from "./abstract";
export class ClientAPI extends AbstractAPI {
tanks() {
return fetchPB("/api/tanks/all.pb", Tanks);
}
tankList() {
return fetchPB("/api/tanks/list.pb", TankList);
}
tank(id: string) {
return fetchPB(`/api/tanks/${id}.pb`, Tank);
protected _avatars() {
return fetchPB("/api/avatars/all.pb", Avatars);
}
}
@@ -8,11 +8,12 @@ import Navbar from "../components/Navbar.astro";
import type { Color } from "../core/ui/color";
interface Props {
locale: string;
accent: Color;
noNavbar?: boolean;
}
const { accent, noNavbar } = Astro.props;
const { accent, noNavbar, locale } = Astro.props;
---
<html>
@@ -20,8 +21,12 @@ const { accent, noNavbar } = Astro.props;
<BaseMeta />
<ColorScale color="mauve" />
<ColorScale color={accent} />
<ColorAlias color="mauve" alias="gray" />
<ColorScale color="black" alpha />
<ColorScale color="white" alpha />
<ColorScale color={accent} />
<ColorAlias color={accent} alias="accent" />
<BaseStyles />
@@ -31,6 +36,7 @@ const { accent, noNavbar } = Astro.props;
display: flex;
flex-direction: column;
gap: var(--space-5);
min-height: 100dvh;
background-color: var(--gray-1);
color: var(--gray-12);
@@ -46,6 +52,6 @@ const { accent, noNavbar } = Astro.props;
<slot />
{(<Footer />)}
<Footer {locale} />
</body>
</html>
@@ -11,9 +11,10 @@ import BaseLayout from "../../../layouts/BaseLayout.astro";
export { getStaticPaths } from "../index.astro";
const root = `/src/pages/api`;
const globbed = import.meta.glob("/src/pages/api/**/*.ts", {
const globbed = import.meta.glob("/src/pages/api/**/*", {
eager: true,
query: "?raw",
import: "default",
});
const paths: {
path: string;
@@ -0,0 +1,19 @@
import { AvatarCard } from "../../../components/AvatarCard";
import { api } from "../../../core/api/dynamic";
import { useAwait } from "../../../hooks/useAwait";
interface Props {
skeleton?: boolean;
}
export function Page({ skeleton }: Props) {
const { avatars } = useAwait(() => api.avatars(), "avatars");
return (
<div className="avatars">
{avatars.map((avatar) => (
<AvatarCard key={avatar.name} avatar={avatar} />
))}
</div>
);
}
@@ -1,41 +1,22 @@
---
import { ProfileAvatarComponent_Category } from "@protos/blitz_static_profile_avatar_component";
import { Grade } from "@protos/blitz_static_standard_grades_enum";
import { api } from "../../../core/api/dynamic";
import BaseLayout from "../../../layouts/BaseLayout.astro";
import { Page } from "./_index";
export { getStaticPaths } from "../index.astro";
const { avatars } = await api.avatars();
const { locale } = Astro.props;
---
<BaseLayout accent="gold">
{
avatars.map((avatar) => {
return (
<>
<h3>
{avatar.stuff_ui!.display_name} {Grade[avatar.stuff_ui!.grade]}{" "}
{ProfileAvatarComponent_Category[avatar.profile_avatar!.category]}
</h3>
{avatar.stuff_ui!.description && (
<p>{avatar.stuff_ui!.description}</p>
)}
{avatar.stuff_ui!.obtaining_methods && (
<p>{avatar.stuff_ui!.obtaining_methods}</p>
)}
{avatar.stuff_ui!.features && <p>{avatar.stuff_ui!.features}</p>}
<img width="128" src={`/api/avatars/${avatar.name}.webp`} />
{avatar.sellable && (
<pre>{JSON.stringify(avatar.sellable, null, 2)}</pre>
)}
<br />
</>
);
})
<style is:global>
.avatars {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--space-7), 1fr));
gap: var(--space-4);
}
</style>
<BaseLayout {locale} accent="gold">
<Page client:only="react">
<Page skeleton slot="fallback" />
</Page>
</BaseLayout>
@@ -21,12 +21,10 @@ export const getStaticPaths = (() => {
const { locale } = Astro.props;
---
<BaseLayout accent="amber">
<BaseLayout accent="amber" noNavbar>
<ColorScale color="purple" slot="head" />
<ColorScale color="gold" slot="head" />
<ColorScale color="lime" slot="head" />
<ColorScale color="black" alpha slot="head" />
<ColorScale color="white" alpha slot="head" />
<HomeHero {locale} />
@@ -5,12 +5,9 @@ import { TankopediaToy } from "../../../../components/TankopediaToy";
import { api } from "../../../../core/api/dynamic";
import { characteristicsOrder } from "../../../../core/tankopedia/characteristicsOrder";
import { computeCharacteristics } from "../../../../core/tankopedia/computeCharacteristics";
import type { TerrainHardness } from "../../../../core/tankopedia/tankState";
import { TerrainHardness } from "../../../../core/tankopedia/tankState";
import { useAwait } from "../../../../hooks/useAwait";
import {
Tankopedia,
type TankopediaCompare,
} from "../../../../stores/tankopedia";
import { Tankopedia, TankopediaCompare } from "../../../../stores/tankopedia";
export function Page({ id }: { id: string }) {
return (
@@ -0,0 +1,15 @@
import type { APIContext, GetStaticPaths } from "astro";
import { api } from "../../../core/api/dynamic";
export const getStaticPaths = (async () => {
const { avatars } = await api.avatars();
return avatars.map((avatar) => ({
params: { id: avatar.name },
}));
}) satisfies GetStaticPaths;
export async function GET({ params }: APIContext<never, { id: string }>) {
const avatar = await api.avatar(params.id);
return Response.json(avatar);
}
@@ -1,18 +1,9 @@
import type { APIContext, GetStaticPaths } from "astro";
import type { APIContext } from "astro";
import { api } from "../../../core/api/dynamic";
import { imageProxy } from "../../../core/api/imageProxy";
export const getStaticPaths = (async () => {
const { avatars } = await api.avatars();
export { getStaticPaths } from "./[id].json";
return avatars.map((avatar) => ({
params: { id: avatar.name },
}));
}) satisfies GetStaticPaths;
/**
* One day I will write documentation for this
*/
export async function GET({ params }: APIContext<never, { id: string }>) {
const avatar = await api.avatar(params.id);
return await imageProxy(avatar.profile_avatar!.avatar);
@@ -0,0 +1,6 @@
import { api } from "../../../core/api/dynamic";
export async function GET() {
const avatars = await api.avatars();
return Response.json(avatars);
}
@@ -0,0 +1,7 @@
import { api } from "../../../core/api/dynamic";
import { Avatars } from "../../../protos/avatars";
export async function GET() {
const avatars = await api.avatars();
return new Response(Avatars.encode(avatars).finish());
}
@@ -0,0 +1,7 @@
import { api } from "../../../core/api/dynamic";
export async function GET() {
const { avatars } = await api.avatars();
const list = avatars.map((avatar) => avatar.name);
return Response.json(list);
}
@@ -0,0 +1,15 @@
import type { APIContext, GetStaticPaths } from "astro";
import { api } from "../../../core/api/dynamic";
export const getStaticPaths = (async () => {
const { backgrounds } = await api.backgrounds();
return backgrounds.map((background) => ({
params: { id: background.name },
}));
}) satisfies GetStaticPaths;
export async function GET({ params }: APIContext<never, { id: string }>) {
const background = await api.background(params.id);
return Response.json(background);
}
@@ -1,18 +1,9 @@
import type { APIContext, GetStaticPaths } from "astro";
import type { APIContext } from "astro";
import { api } from "../../../core/api/dynamic";
import { imageProxy } from "../../../core/api/imageProxy";
export const getStaticPaths = (async () => {
const { backgrounds } = await api.backgrounds();
export { getStaticPaths } from "./[id].json";
return backgrounds.map((background) => ({
params: { id: background.name },
}));
}) satisfies GetStaticPaths;
/**
* One day I will write documentation for this
*/
export async function GET({ params }: APIContext<never, { id: string }>) {
const background = await api.background(params.id);
return await imageProxy(background.profile_background!.background);
@@ -0,0 +1,6 @@
import { api } from "../../../core/api/dynamic";
export async function GET() {
const { backgrounds } = await api.backgrounds();
return Response.json(backgrounds);
}
@@ -0,0 +1,7 @@
import { api } from "../../../core/api/dynamic";
export async function GET() {
const { backgrounds } = await api.backgrounds();
const list = backgrounds.map((background) => background.name);
return Response.json(list);
}
@@ -2,8 +2,9 @@ import { TankClass, TankType } from "@blitzkit/core";
import { Nation } from "@protos/blitz_static_tank_component";
import type { APIContext, GetStaticPaths } from "astro";
import { GunType } from "../../../core/tankopedia/characteristics";
import { GET as _GET } from "./all.json";
const enums = {
export const ENUMS = {
nation: Nation,
"tank-class": TankClass,
"tank-type": TankType,
@@ -11,29 +12,12 @@ const enums = {
};
export const getStaticPaths = (() => {
return Object.keys(enums).map((name) => ({ params: { enum: name } }));
return Object.keys(ENUMS).map((name) => ({ params: { enum: name } }));
}) satisfies GetStaticPaths;
/**
* Enumerations used within BlitzKit, for nominal development reference only.
*
* DO NOT programmatically use this API as it is designed solely for human
* reference.
*
* DO NOT blindly copy entries as naming conventions vary. Most enums come
* directly from the game which uses a SCREAMING_SNAKE_CASE convention while
* BlitzKit uses PascalCase.
*/
export function GET({
export async function GET({
params,
}: APIContext<never, { enum: keyof typeof enums }>) {
const map: Record<number, string> = {};
for (const key in enums[params.enum]) {
const value = enums[params.enum][key];
if (typeof value !== "string") continue;
map[key] = value;
}
return Response.json(map);
}: APIContext<never, { enum: keyof typeof ENUMS }>) {
const enums = (await _GET().json()) as Record<string, Record<number, string>>;
return Response.json(enums[params.enum]);
}
@@ -0,0 +1,20 @@
import { ENUMS } from "./[enum].json";
export function GET() {
const enums: Record<string, Record<number, string>> = {};
for (const name in ENUMS) {
const _enum = ENUMS[name as keyof typeof ENUMS];
const map: Record<number, string> = {};
for (const key in _enum) {
const value = _enum[key];
if (typeof value !== "string") continue;
map[key] = value;
}
enums[name] = map;
}
return Response.json(enums);
}
@@ -0,0 +1,6 @@
import { ENUMS } from "./[enum].json";
export function GET() {
const list = Object.keys(ENUMS);
return Response.json(list);
}