mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-04 08:20:37 +01:00
Compare commits
8
Commits
c0c6a21e43
...
dd96556e2a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd96556e2a | ||
|
|
a7ee51526a | ||
|
|
610b59f4c6 | ||
|
|
63ce327458 | ||
|
|
6ec7a50320 | ||
|
|
60a29ef2e7 | ||
|
|
52c3a0abe0 | ||
|
|
5bbe5c2720 |
No files matched your search
@@ -16,7 +16,8 @@
|
||||
"t": "{value}t",
|
||||
"bhp_t": "{value}bhp/t",
|
||||
"deg_s": "{value}°/s",
|
||||
"%": "{value}%"
|
||||
"%": "{value}%",
|
||||
"x": "x{value}"
|
||||
},
|
||||
|
||||
"tools": {
|
||||
@@ -45,5 +46,9 @@
|
||||
"home": {
|
||||
"search": "Search tanks, players, tools...",
|
||||
"credit": "by TrèsAbhi"
|
||||
},
|
||||
|
||||
"avatars": {
|
||||
"search": "Search avatars..."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="70" height="35" viewBox="0 0 70 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M70 32L64 35H0L70 0V32ZM0 32V0H64L0 32Z" fill="#EEEEF0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 170 B |
@@ -18,6 +18,12 @@
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.count {
|
||||
position: absolute;
|
||||
bottom: var(--space-3);
|
||||
right: var(--space-3);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-label {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { literals } from "@blitzkit/i18n";
|
||||
import { useStrings } from "../../hooks/useStrings";
|
||||
import type { Avatar } from "../../protos/avatar";
|
||||
import type { SkeletonProps } from "../../types/SkeletonProps";
|
||||
import { Badge } from "../Badge";
|
||||
import { Text } from "../Text";
|
||||
import "./index.css";
|
||||
|
||||
@@ -29,6 +32,7 @@ interface SeriesProps {
|
||||
|
||||
function Series({ avatars }: SeriesProps) {
|
||||
const count = Math.min(avatars.length, MAX_CARDS);
|
||||
const strings = useStrings();
|
||||
|
||||
return (
|
||||
<div className="avatar-series">
|
||||
@@ -45,6 +49,12 @@ function Series({ avatars }: SeriesProps) {
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{avatars.length > 1 && (
|
||||
<Badge color="gray" highContrast className="count">
|
||||
{literals(strings.units.x, { value: avatars.length })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.avatar-controls {
|
||||
display: flex;
|
||||
|
||||
.search {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { useRef } from "react";
|
||||
import { useStrings } from "../../hooks/useStrings";
|
||||
import { Avatars } from "../../stores/avatars";
|
||||
import { TextField } from "../TextField";
|
||||
import "./index.css";
|
||||
|
||||
export function AvatarControls() {
|
||||
const strings = useStrings();
|
||||
const search = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<div className="avatar-controls">
|
||||
<TextField
|
||||
ref={search}
|
||||
className="search"
|
||||
placeholder={strings.avatars.search}
|
||||
onChange={(event) => {
|
||||
const trimmed = event.target.value.trim();
|
||||
|
||||
Avatars.mutate((draft) => {
|
||||
if (trimmed.length === 0) {
|
||||
draft.search = null;
|
||||
} else {
|
||||
draft.search = trimmed;
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MagnifyingGlassIcon />
|
||||
</TextField>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.avatars-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--space-7), 1fr));
|
||||
gap: var(--space-5) var(--space-4);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { useMemo } from "react";
|
||||
import { api } from "../../core/api/dynamic";
|
||||
import { useAwait } from "../../hooks/useAwait";
|
||||
import { useGameStrings } from "../../hooks/useGameStrings";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import type { Avatar } from "../../protos/avatar";
|
||||
import { Avatars } from "../../stores/avatars";
|
||||
import { AvatarCard } from "../AvatarCard";
|
||||
import { IncrementalLoader } from "../IncrementalLoader";
|
||||
import "./index.css";
|
||||
|
||||
const { go } = fuzzysort;
|
||||
|
||||
const indexingPattern = /.+_(\d+)$/;
|
||||
|
||||
export function AvatarsList() {
|
||||
const locale = useLocale();
|
||||
const search = Avatars.use((state) => state.search);
|
||||
const { avatars } = useAwait(() => api.avatars(), "avatars");
|
||||
const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity");
|
||||
const groups = useMemo(() => {
|
||||
const groups = new Map<string, Avatar[]>();
|
||||
|
||||
for (const avatar of avatars) {
|
||||
const name =
|
||||
profileAvatarEntityStrings[avatar.stuff_ui!.display_name] ??
|
||||
avatar.stuff_ui!.display_name;
|
||||
|
||||
if (groups.has(name)) {
|
||||
groups.get(name)!.push(avatar);
|
||||
} else {
|
||||
groups.set(name, [avatar]);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, []);
|
||||
const ordered = useMemo(() => {
|
||||
const array = Array.from(groups.entries());
|
||||
return array
|
||||
.sort((a, b) => a[0].localeCompare(b[0], locale))
|
||||
.map(([name, avatars]) => ({
|
||||
key: name,
|
||||
name,
|
||||
avatars: avatars.sort((a, b) => {
|
||||
const gradeA = a.stuff_ui!.grade;
|
||||
const gradeB = b.stuff_ui!.grade;
|
||||
let diff = gradeA - gradeB;
|
||||
|
||||
if (diff === 0) {
|
||||
const matchesA = a.name.match(indexingPattern);
|
||||
const matchesB = b.name.match(indexingPattern);
|
||||
|
||||
if (matchesA && matchesB) {
|
||||
const indexA = Number(matchesA[1]);
|
||||
const indexB = Number(matchesB[1]);
|
||||
|
||||
diff = indexA - indexB;
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
}),
|
||||
}));
|
||||
}, []);
|
||||
const filtered = useMemo(() => {
|
||||
if (search === null) return ordered;
|
||||
|
||||
const searched = go(search, ordered, { keys: ["name"] });
|
||||
|
||||
return searched.map((result) => result.obj);
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<div className="avatars-list">
|
||||
<IncrementalLoader
|
||||
initial={40}
|
||||
intermediate={10}
|
||||
data={filtered}
|
||||
Component={AvatarCard}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.badge {
|
||||
border-radius: var(--max-size);
|
||||
box-shadow: var(--shadow-aggressive);
|
||||
|
||||
padding: var(--space-2) var(--space-3);
|
||||
|
||||
font-size: var(--font-size-minor);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import type { Color } from "../../core/ui/color";
|
||||
import "./index.css";
|
||||
|
||||
export interface BadgeProps extends ComponentProps<"div"> {
|
||||
color?: Color | "accent";
|
||||
highContrast?: boolean;
|
||||
}
|
||||
|
||||
export function Badge({
|
||||
className,
|
||||
style,
|
||||
color = "accent",
|
||||
highContrast,
|
||||
...props
|
||||
}: BadgeProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames("badge", className)}
|
||||
style={{
|
||||
backgroundColor: `var(--${color}-${highContrast ? 12 : 9})`,
|
||||
color: `var(--gray-${highContrast ? 1 : 12})`,
|
||||
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import "@fontsource-variable/roboto-flex";
|
||||
--space-8: 16rem;
|
||||
--space-9: 32rem;
|
||||
|
||||
--page-width: 52rem;
|
||||
--page-width: 64rem;
|
||||
|
||||
--max-size: max(100vw, 100vh);
|
||||
|
||||
@@ -74,8 +74,11 @@ import "@fontsource-variable/roboto-flex";
|
||||
--shadow-aggressive: var(--space-1) var(--space-1) var(--space-4)
|
||||
var(--black-a11);
|
||||
--shadow-subtle: 0 var(--space-1) var(--space-3) var(--black-a5);
|
||||
--shadow-none: 0 0 0 transparent;
|
||||
|
||||
--blur-glass: blur(4rem);
|
||||
--blur-tint: blur(16rem);
|
||||
--blur-tint: blur(8rem);
|
||||
|
||||
--duration-brief: 200ms;
|
||||
}
|
||||
</style>
|
||||
@@ -3,17 +3,17 @@ import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import type { SkeletonProps } from "../../types/SkeletonProps";
|
||||
import "./index.css";
|
||||
|
||||
export interface IncrementalLoaderProps<Props> {
|
||||
export interface IncrementalLoaderProps<Props extends { key: string }> {
|
||||
initial: number;
|
||||
intermediate: number;
|
||||
rate?: number;
|
||||
|
||||
skeleton?: boolean;
|
||||
data: Props[];
|
||||
Component: (props: SkeletonProps<Props>) => ReactNode;
|
||||
Component: (props: SkeletonProps<Omit<Props, "key">>) => ReactNode;
|
||||
}
|
||||
|
||||
export function IncrementalLoader<Props>({
|
||||
export function IncrementalLoader<Props extends { key: string }>({
|
||||
initial,
|
||||
rate = 2,
|
||||
data,
|
||||
@@ -24,10 +24,12 @@ export function IncrementalLoader<Props>({
|
||||
const [count, setCount] = useState(initial);
|
||||
const sliced = data.slice(0, count);
|
||||
|
||||
useEffect(() => setCount(initial), [data, initial]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!skeleton &&
|
||||
sliced.map((item, index) => <Component key={index} {...item} />)}
|
||||
sliced.map(({ key, ...props }) => <Component key={key} {...props} />)}
|
||||
|
||||
{times(
|
||||
skeleton
|
||||
|
||||
@@ -11,7 +11,7 @@ const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<style>
|
||||
.navbar {
|
||||
#navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -21,7 +21,15 @@ const { locale } = Astro.props;
|
||||
|
||||
z-index: 1;
|
||||
backdrop-filter: var(--blur-glass) brightness(25%);
|
||||
box-shadow: var(--shadow-subtle);
|
||||
background-color: var(--accent-2);
|
||||
box-shadow: var(--shadow-none);
|
||||
|
||||
transition-duration: var(--duration-brief);
|
||||
|
||||
&[data-sticky="true"] {
|
||||
background-color: var(--gray-a2);
|
||||
box-shadow: var(--shadow-subtle);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
@@ -55,7 +63,18 @@ const { locale } = Astro.props;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="navbar">
|
||||
<script>
|
||||
const navbar = document.getElementById("navbar")!;
|
||||
|
||||
function handleScroll() {
|
||||
navbar.dataset.sticky = `${window.scrollY > 0}`;
|
||||
}
|
||||
|
||||
handleScroll();
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
</script>
|
||||
|
||||
<div id="navbar">
|
||||
<div class="content">
|
||||
<div class="side">
|
||||
<LinkI18n {locale} href="/" className="action" tabIndex={0}>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<style>
|
||||
.section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 var(--space-4);
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
max-width: var(--page-width);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="section">
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
.section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 var(--space-4);
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
max-width: var(--page-width);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import "./index.css";
|
||||
|
||||
export function Section({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={classNames("section", className)} {...props}>
|
||||
<div className="content">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import type { Color } from "../../core/ui/color";
|
||||
import "./index.css";
|
||||
|
||||
export interface TextProps extends ComponentProps<"span"> {
|
||||
color?: Color;
|
||||
color?: Color | "accent";
|
||||
weight?: "light" | "regular" | "medium" | "bold" | "black";
|
||||
lowContrast?: boolean;
|
||||
size?: "regular" | "minor" | "major";
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
box-sizing: border-box;
|
||||
|
||||
gap: var(--space-3);
|
||||
margin: 0 var(--space-4);
|
||||
|
||||
border-radius: var(--max-size);
|
||||
|
||||
@@ -16,6 +15,10 @@
|
||||
|
||||
&[data-has-icon="true"] {
|
||||
padding-left: var(--space-3);
|
||||
|
||||
input {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
@@ -38,6 +41,7 @@
|
||||
font-size: inherit;
|
||||
font-weight: medium;
|
||||
padding: var(--space-3) 0;
|
||||
padding-left: var(--space-4);
|
||||
|
||||
color: var(--gray-12);
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
|
||||
---
|
||||
|
||||
<style>
|
||||
.tool-hero {
|
||||
.tool-banner {
|
||||
background-size: cover;
|
||||
margin-bottom: calc(var(--space-5) - var(--space-4));
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
@@ -30,6 +31,10 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
|
||||
|
||||
backdrop-filter: var(--blur-tint);
|
||||
|
||||
@media (width > 40rem) {
|
||||
padding: var(--space-6) var(--space-4);
|
||||
}
|
||||
|
||||
.centered {
|
||||
flex: 1;
|
||||
max-width: var(--page-width);
|
||||
@@ -45,7 +50,6 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-image: url(/assets/patterns/avatars.svg);
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 25%,
|
||||
@@ -60,14 +64,20 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
|
||||
</style>
|
||||
|
||||
<div
|
||||
class="tool-hero"
|
||||
class="tool-banner"
|
||||
style={{
|
||||
backgroundImage: `url(/assets/tools/${tool.id}.webp)`,
|
||||
backgroundPosition: tool.background_position ?? "top",
|
||||
backgroundPosition: tool.banner_background_position ?? "top",
|
||||
}}
|
||||
>
|
||||
<div class="content">
|
||||
<div class="pattern"></div>
|
||||
<div
|
||||
class="pattern"
|
||||
style={{
|
||||
backgroundImage: `url(/assets/patterns/${tool.id}.svg)`,
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="centered">
|
||||
<Heading weight="medium" size="2">
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Strings } from "@blitzkit/i18n";
|
||||
import { Avatars } from "../../protos/avatars";
|
||||
import { AbstractAPI } from "./abstract";
|
||||
|
||||
//@ts-expect-error
|
||||
export class ClientAPI extends AbstractAPI {
|
||||
protected _avatars() {
|
||||
return fetchPB("/api/avatars/all.pb", Avatars);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type {
|
||||
GetStaticPathsItem,
|
||||
GetStaticPathsOptions,
|
||||
GetStaticPathsResult,
|
||||
} from "astro";
|
||||
|
||||
type Promisable<T> = T | Promise<T>;
|
||||
|
||||
export function mixStaticPaths<
|
||||
TypeA extends GetStaticPathsItem,
|
||||
TypeB extends GetStaticPathsItem
|
||||
>(
|
||||
a: (options: GetStaticPathsOptions) => Promisable<TypeA[]>,
|
||||
b: (options: GetStaticPathsOptions) => Promisable<TypeB[]>
|
||||
) {
|
||||
return async function (options: GetStaticPathsOptions) {
|
||||
const pathsA = await a(options);
|
||||
const pathsB = await b(options);
|
||||
const paths: GetStaticPathsResult = [];
|
||||
|
||||
for (const pathA of pathsA) {
|
||||
for (const pathB of pathsB) {
|
||||
paths.push({
|
||||
params: { ...pathA.params, ...pathB.params },
|
||||
props: { ...pathA.props, ...pathB.props },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return paths as (TypeA & TypeB)[];
|
||||
};
|
||||
}
|
||||
@@ -252,7 +252,7 @@ export const characteristics = {
|
||||
);
|
||||
},
|
||||
|
||||
gun_depression({ parameters, tank }) {
|
||||
gun_depression({ parameters }) {
|
||||
return -radToDeg(
|
||||
applyPitchYawLimits(
|
||||
-Math.PI / 2,
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface Tool {
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
|
||||
background_position: "top" | "center" | "bottom";
|
||||
banner_background_position: "top" | "center" | "bottom";
|
||||
}
|
||||
|
||||
export const tools: Record<string, Tool> = {
|
||||
@@ -19,24 +19,28 @@ export const tools: Record<string, Tool> = {
|
||||
button: "purple",
|
||||
significant: true,
|
||||
|
||||
background_position: "top",
|
||||
banner_background_position: "top",
|
||||
},
|
||||
|
||||
avatars: {
|
||||
id: "avatars",
|
||||
button: "gold",
|
||||
|
||||
background_position: "bottom",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
|
||||
backgrounds: {
|
||||
id: "backgrounds",
|
||||
button: "gold",
|
||||
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
|
||||
api: {
|
||||
id: "api",
|
||||
disabled: true,
|
||||
button: "lime",
|
||||
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
};
|
||||
@@ -29,7 +29,10 @@ const { accent, noNavbar, locale } = Astro.props;
|
||||
<ColorScale color="white" alpha />
|
||||
|
||||
<ColorScale color={accent} />
|
||||
<ColorScale color={accent} alpha />
|
||||
<ColorAlias color={accent} alias="accent" />
|
||||
<ColorAlias color={accent} alpha alias="accent" />
|
||||
|
||||
<BaseStyles />
|
||||
|
||||
<style>
|
||||
|
||||
@@ -10,6 +10,7 @@ import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const root = `/src/pages/api`;
|
||||
const globbed = import.meta.glob("/src/pages/api/**/*", {
|
||||
eager: true,
|
||||
@@ -47,7 +48,7 @@ for (const fullPath in globbed) {
|
||||
paths.sort((a, b) => a.path.localeCompare(b.path));
|
||||
---
|
||||
|
||||
<BaseLayout accent="amber">
|
||||
<BaseLayout {locale} accent="amber">
|
||||
<h1>api</h1>
|
||||
|
||||
{
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { AvatarCard } from "../../../components/AvatarCard";
|
||||
import { IncrementalLoader } from "../../../components/IncrementalLoader";
|
||||
import { api } from "../../../core/api/dynamic";
|
||||
import { useAwait } from "../../../hooks/useAwait";
|
||||
import { useGameStrings } from "../../../hooks/useGameStrings";
|
||||
import { LocaleProvider, useLocale } from "../../../hooks/useLocale";
|
||||
import type { Avatar } from "../../../protos/avatar";
|
||||
import { AvatarControls } from "../../../components/AvatarControls";
|
||||
import { AvatarsList } from "../../../components/AvatarsList";
|
||||
import { Section } from "../../../components/Section";
|
||||
import { LocaleProvider } from "../../../hooks/useLocale";
|
||||
|
||||
interface PageProps extends ContentProps {
|
||||
locale: string;
|
||||
@@ -23,47 +19,16 @@ interface ContentProps {
|
||||
skeleton?: boolean;
|
||||
}
|
||||
|
||||
function Content({ skeleton }: ContentProps) {
|
||||
const locale = useLocale();
|
||||
const { avatars } = useAwait(() => api.avatars(), "avatars");
|
||||
const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity");
|
||||
const groups = useMemo(() => {
|
||||
const groups = new Map<string, Avatar[]>();
|
||||
|
||||
for (const avatar of avatars) {
|
||||
const name =
|
||||
profileAvatarEntityStrings[avatar.stuff_ui!.display_name] ??
|
||||
avatar.stuff_ui!.display_name;
|
||||
|
||||
if (groups.has(name)) {
|
||||
groups.get(name)!.push(avatar);
|
||||
} else {
|
||||
groups.set(name, [avatar]);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, []);
|
||||
const ordered = useMemo(() => {
|
||||
const array = Array.from(groups.entries());
|
||||
return array
|
||||
.sort((a, b) => a[0].localeCompare(b[0], locale))
|
||||
.map(([name, avatars]) => ({
|
||||
name,
|
||||
avatars: avatars.sort((a, b) => {
|
||||
return a.stuff_ui!.grade - b.stuff_ui!.grade;
|
||||
}),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
function Content({}: ContentProps) {
|
||||
return (
|
||||
<div className="avatars">
|
||||
<IncrementalLoader
|
||||
initial={40}
|
||||
intermediate={10}
|
||||
data={ordered}
|
||||
Component={AvatarCard}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<Section>
|
||||
<AvatarControls />
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<AvatarsList />
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
import Section from "../../../components/Section.astro";
|
||||
import ToolBanner from "../../../components/ToolBanner.astro";
|
||||
import { tools } from "../../../core/tools/tools";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
@@ -10,20 +9,10 @@ export { getStaticPaths } from "../index.astro";
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<style is:global>
|
||||
.avatars {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--space-7), 1fr));
|
||||
gap: var(--space-5) var(--space-4);
|
||||
}
|
||||
</style>
|
||||
|
||||
<BaseLayout {locale} accent="gold">
|
||||
<ToolBanner {locale} tool={tools.avatars} slot="pre-app" />
|
||||
|
||||
<Section>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</Section>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</BaseLayout>
|
||||
@@ -5,10 +5,11 @@ import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const { backgrounds } = await api.backgrounds();
|
||||
---
|
||||
|
||||
<BaseLayout accent="amber">
|
||||
<BaseLayout {locale} accent="amber">
|
||||
{
|
||||
backgrounds.map((background) => {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ 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.astro";
|
||||
import { Section } from "../../components/Section";
|
||||
import ToolCard from "../../components/ToolCard.astro";
|
||||
import { tools } from "../../core/tools/tools";
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
|
||||
@@ -19,8 +19,8 @@ export function Page({ id }: { id: string }) {
|
||||
|
||||
function Content({ id }: { id: string }) {
|
||||
const tank = useAwait(() => api.tank(id), `tank-${id}`);
|
||||
const tankList = useAwait(() => api.tankList(), "tank-list");
|
||||
const otherTanks = useAwait(() => api.tanks(), "tanks");
|
||||
// const tankList = useAwait(() => api.tankList(), "tank-list");
|
||||
// const otherTanks = useAwait(() => api.tanks(), "tanks");
|
||||
|
||||
Tankopedia.useInitialization(tank.tank!);
|
||||
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
---
|
||||
import "@radix-ui/colors/violet-dark.css";
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { api } from "../../../../core/api/dynamic";
|
||||
import { mixStaticPaths } from "../../../../core/astro/mixStaticPaths";
|
||||
import BaseLayout from "../../../../layouts/BaseLayout.astro";
|
||||
import { getStaticPaths as _getStaticPaths } from "../index.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
||||
const { list } = await api.tankList();
|
||||
|
||||
return list.map((tank) => ({
|
||||
params: { slug: tank.slug },
|
||||
props: { id: tank.id },
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
});
|
||||
|
||||
const { id } = Astro.props;
|
||||
const { id, locale } = Astro.props;
|
||||
---
|
||||
|
||||
<BaseLayout accent="violet">
|
||||
<BaseLayout {locale} accent="violet">
|
||||
<Page {id} client:load />
|
||||
</BaseLayout>
|
||||
@@ -1,14 +1,19 @@
|
||||
---
|
||||
import "@radix-ui/colors/violet-dark.css";
|
||||
import ToolBanner from "../../../components/ToolBanner.astro";
|
||||
import { api } from "../../../core/api/dynamic";
|
||||
import { tools } from "../../../core/tools/tools";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const tankList = await api.tankList();
|
||||
---
|
||||
|
||||
<BaseLayout accent="violet">
|
||||
<BaseLayout {locale} accent="purple">
|
||||
<ToolBanner {locale} tool={tools.tanks} slot="pre-app" />
|
||||
|
||||
<h1>tanks</h1>
|
||||
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
interface Avatars {
|
||||
search: string | null;
|
||||
}
|
||||
|
||||
export const Avatars = new Varuna<Avatars>({
|
||||
search: null,
|
||||
});
|
||||
Reference in new issue
Block a user