mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-04 00:10:38 +01:00
Compare commits
7
Commits
4a6f5845f9
...
c0c6a21e43
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c6a21e43 | ||
|
|
a7f93698a6 | ||
|
|
1a18058228 | ||
|
|
67024daa25 | ||
|
|
de6137d1a5 | ||
|
|
11e87bd519 | ||
|
|
3b4e089756 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
<svg width="30" height="52" viewBox="0 0 30 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.8564 44V52H0V36L13.8564 44ZM30 52H16.1436V44L30 36V52ZM28.8564 18V34L15 42L1.14355 34V18L15 10L28.8564 18ZM13.8564 8L0 16V0H13.8564V8ZM30 16L16.1436 8V0H30V16Z" fill="#EEEEF0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@@ -4,15 +4,20 @@
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
|
||||
.avatar-image {
|
||||
.avatar-series {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 7 / 8;
|
||||
border-radius: var(--space-3);
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
.avatar-entry {
|
||||
position: absolute;
|
||||
|
||||
box-shadow: var(--shadow-interactive), var(--shadow-subtle);
|
||||
border-radius: var(--space-3);
|
||||
box-shadow: var(--shadow-interactive), var(--shadow-aggressive);
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-label {
|
||||
|
||||
@@ -1,24 +1,50 @@
|
||||
import { useGameStrings } from "../../hooks/useGameStrings";
|
||||
import type { Avatar } from "../../protos/avatar";
|
||||
import type { SkeletonProps } from "../../types/SkeletonProps";
|
||||
import { Text } from "../Text";
|
||||
import "./index.css";
|
||||
|
||||
interface Props {
|
||||
avatar: Avatar;
|
||||
name: string;
|
||||
avatars: Avatar[];
|
||||
}
|
||||
|
||||
export function AvatarCard({ avatar }: Props) {
|
||||
const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity");
|
||||
const MAX_CARDS = 3;
|
||||
const CARD_TRIM = "var(--space-3)";
|
||||
|
||||
export function AvatarCard(props: SkeletonProps<Props>) {
|
||||
return (
|
||||
<div className="avatar-card">
|
||||
<div
|
||||
className="avatar-image"
|
||||
style={{ backgroundImage: `url(/api/avatars/${avatar.name}.webp)` }}
|
||||
/>
|
||||
{!props.skeleton && <Series avatars={props.avatars} />}
|
||||
|
||||
<Text align="center" className="avatar-label">
|
||||
{profileAvatarEntityStrings[avatar.stuff_ui!.display_name]}
|
||||
{props.skeleton ? "LOADING" : props.name}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SeriesProps {
|
||||
avatars: Avatar[];
|
||||
}
|
||||
|
||||
function Series({ avatars }: SeriesProps) {
|
||||
const count = Math.min(avatars.length, MAX_CARDS);
|
||||
|
||||
return (
|
||||
<div className="avatar-series">
|
||||
{avatars.slice(-MAX_CARDS).map((avatar, index) => (
|
||||
<div
|
||||
className="avatar-entry"
|
||||
style={{
|
||||
filter: `brightness(${(index + 1) / count})`,
|
||||
top: `calc(${index} * ${CARD_TRIM})`,
|
||||
left: `calc(${index} * ${CARD_TRIM})`,
|
||||
width: `calc(100% - ${CARD_TRIM} * ${count - 1})`,
|
||||
height: `calc(100% - ${CARD_TRIM} * ${count - 1})`,
|
||||
backgroundImage: `url(/api/avatars/${avatar.name}.webp)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,13 @@ import "@fontsource-variable/roboto-flex";
|
||||
--font-size-minor: 0.75rem;
|
||||
--font-size-major: 1.25rem;
|
||||
|
||||
--font-size-heading-1: 3rem;
|
||||
--font-size-heading-2: 2rem;
|
||||
--font-size-heading-3: 1.5rem;
|
||||
--font-size-heading-4: 1.25rem;
|
||||
--font-size-heading-5: 1rem;
|
||||
--font-size-heading-6: 0.75rem;
|
||||
|
||||
--font-family-regular: "Roboto Flex Variable", sans-serif;
|
||||
--font-family-mono: "JetBrains Mono Variable", monospace;
|
||||
|
||||
@@ -64,9 +71,11 @@ import "@fontsource-variable/roboto-flex";
|
||||
var(--shadow-interactive-shine), var(--shadow-interactive-shadow),
|
||||
var(--shadow-interactive-background);
|
||||
|
||||
--shadow-subtle: var(--space-1) var(--space-1) var(--space-4)
|
||||
--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);
|
||||
|
||||
--blur-glass: blur(4rem);
|
||||
--blur-tint: blur(16rem);
|
||||
}
|
||||
</style>
|
||||
@@ -19,7 +19,6 @@ const strings = await api.strings(locale);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
margin-top: calc(var(--space-4) * -1);
|
||||
padding: var(--space-5) var(--space-5);
|
||||
|
||||
background-color: var(--gray-3);
|
||||
@@ -48,14 +47,8 @@ const strings = await api.strings(locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="footer-spacer"></div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="content">
|
||||
<div class="logo">
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
.heading {
|
||||
&[data-size="1"] {
|
||||
font-size: var(--font-size-heading-1);
|
||||
}
|
||||
|
||||
&[data-size="2"] {
|
||||
font-size: var(--font-size-heading-2);
|
||||
}
|
||||
|
||||
&[data-size="3"] {
|
||||
font-size: var(--font-size-heading-3);
|
||||
}
|
||||
|
||||
&[data-size="4"] {
|
||||
font-size: var(--font-size-heading-4);
|
||||
}
|
||||
|
||||
&[data-size="5"] {
|
||||
font-size: var(--font-size-heading-5);
|
||||
}
|
||||
|
||||
&[data-size="6"] {
|
||||
font-size: var(--font-size-heading-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import { Text, type TextProps } from "../Text";
|
||||
import "./index.css";
|
||||
|
||||
export interface HeadingProps extends Omit<TextProps, "size"> {
|
||||
size?: `${1 | 2 | 3 | 4 | 5 | 6}`;
|
||||
}
|
||||
|
||||
export function Heading({
|
||||
className,
|
||||
weight,
|
||||
size = "1",
|
||||
...props
|
||||
}: HeadingProps) {
|
||||
return (
|
||||
<Text
|
||||
{...props}
|
||||
weight={weight ?? "medium"}
|
||||
className={classNames("heading", className)}
|
||||
data-size={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
.incremental-loader {
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { times } from "lodash-es";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import type { SkeletonProps } from "../../types/SkeletonProps";
|
||||
import "./index.css";
|
||||
|
||||
export interface IncrementalLoaderProps<Props> {
|
||||
initial: number;
|
||||
intermediate: number;
|
||||
rate?: number;
|
||||
|
||||
skeleton?: boolean;
|
||||
data: Props[];
|
||||
Component: (props: SkeletonProps<Props>) => ReactNode;
|
||||
}
|
||||
|
||||
export function IncrementalLoader<Props>({
|
||||
initial,
|
||||
rate = 2,
|
||||
data,
|
||||
skeleton,
|
||||
intermediate,
|
||||
Component,
|
||||
}: IncrementalLoaderProps<Props>) {
|
||||
const [count, setCount] = useState(initial);
|
||||
const sliced = data.slice(0, count);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!skeleton &&
|
||||
sliced.map((item, index) => <Component key={index} {...item} />)}
|
||||
|
||||
{times(
|
||||
skeleton
|
||||
? initial + intermediate
|
||||
: Math.min(intermediate, data.length - count),
|
||||
(index) => (
|
||||
<Item key={index} rate={rate} setCount={setCount}>
|
||||
<Component skeleton />
|
||||
</Item>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ItemProps {
|
||||
children: ReactNode;
|
||||
rate: number;
|
||||
setCount: (setter: (state: number) => number) => void;
|
||||
}
|
||||
|
||||
function Item({ setCount, rate, children }: ItemProps) {
|
||||
const wrapper = useRef<HTMLDivElement>(null);
|
||||
const observer = useRef<IntersectionObserver | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function dispose() {
|
||||
observer.current?.disconnect();
|
||||
observer.current = null;
|
||||
}
|
||||
|
||||
if (!wrapper.current) return;
|
||||
|
||||
if (observer.current === null) {
|
||||
observer.current = new IntersectionObserver(([entry]) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
setCount((state) => state + rate);
|
||||
});
|
||||
|
||||
observer.current.observe(wrapper.current);
|
||||
}
|
||||
|
||||
return dispose;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={wrapper} className="incremental-loader">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,8 +20,8 @@ const { locale } = Astro.props;
|
||||
justify-content: center;
|
||||
|
||||
z-index: 1;
|
||||
backdrop-filter: var(--blur-glass);
|
||||
background-color: var(--black-a5);
|
||||
backdrop-filter: var(--blur-glass) brightness(25%);
|
||||
box-shadow: var(--shadow-subtle);
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
import { api } from "../core/api/dynamic";
|
||||
import type { Tool } from "../core/tools/tools";
|
||||
import { Heading } from "./Heading";
|
||||
import { Text } from "./Text";
|
||||
|
||||
interface Props {
|
||||
locale: string;
|
||||
tool: Tool;
|
||||
}
|
||||
|
||||
const { tool, locale } = Astro.props;
|
||||
const strings = await api.strings(locale);
|
||||
const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
|
||||
---
|
||||
|
||||
<style>
|
||||
.tool-hero {
|
||||
background-size: cover;
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--space-5) var(--space-4);
|
||||
box-sizing: border-box;
|
||||
|
||||
backdrop-filter: var(--blur-tint);
|
||||
|
||||
.centered {
|
||||
flex: 1;
|
||||
max-width: var(--page-width);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-image: url(/assets/patterns/avatars.svg);
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 25%,
|
||||
var(--black-a1),
|
||||
transparent
|
||||
);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
class="tool-hero"
|
||||
style={{
|
||||
backgroundImage: `url(/assets/tools/${tool.id}.webp)`,
|
||||
backgroundPosition: tool.background_position ?? "top",
|
||||
}}
|
||||
>
|
||||
<div class="content">
|
||||
<div class="pattern"></div>
|
||||
|
||||
<div class="centered">
|
||||
<Heading weight="medium" size="2">
|
||||
{toolStrings.name}
|
||||
</Heading>
|
||||
<Text>
|
||||
{toolStrings.slogan}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,10 +83,9 @@ export abstract class AbstractAPI {
|
||||
> = {};
|
||||
protected abstract _groupedGameStrings(
|
||||
locale: string,
|
||||
group: string,
|
||||
prefix: boolean
|
||||
group: string
|
||||
): Promise<Record<string, string>>;
|
||||
async groupedGameStrings(locale: string, group: string, prefix = true) {
|
||||
async groupedGameStrings(locale: string, group: string, prefix: boolean) {
|
||||
this.assertLocale(locale);
|
||||
|
||||
if (this._groupedGameStringsCache[locale] === undefined) {
|
||||
@@ -95,10 +94,19 @@ export abstract class AbstractAPI {
|
||||
|
||||
if (this._groupedGameStringsCache[locale][group] === undefined) {
|
||||
this._groupedGameStringsCache[locale][group] =
|
||||
await this._groupedGameStrings(locale, group, prefix);
|
||||
await this._groupedGameStrings(locale, group);
|
||||
}
|
||||
|
||||
return this._groupedGameStringsCache[locale][group];
|
||||
if (!prefix) return this._groupedGameStringsCache[locale][group];
|
||||
|
||||
const strings: Record<string, string> = {};
|
||||
|
||||
for (const key in this._groupedGameStringsCache[locale][group]) {
|
||||
strings[`${group}__${key}`] =
|
||||
this._groupedGameStringsCache[locale][group][key];
|
||||
}
|
||||
|
||||
return strings;
|
||||
}
|
||||
|
||||
private _avatarCache: Record<string, Avatar> = {};
|
||||
|
||||
@@ -12,29 +12,15 @@ export class ClientAPI extends AbstractAPI {
|
||||
throw new Error("Cannot fetch full game strings on client");
|
||||
}
|
||||
|
||||
protected async _groupedGameStrings(
|
||||
locale: string,
|
||||
group: string,
|
||||
prefix: boolean
|
||||
) {
|
||||
protected async _groupedGameStrings(locale: string, group: string) {
|
||||
const response = await fetch(`/api/game-strings/${locale}/${group}.json`);
|
||||
const json = (await response.json()) as Record<string, string>;
|
||||
|
||||
if (!prefix) return json;
|
||||
|
||||
const strings: Record<string, string> = {};
|
||||
|
||||
for (const key in json) {
|
||||
strings[`${group}__${key}`] = json[key];
|
||||
}
|
||||
|
||||
return strings;
|
||||
return json;
|
||||
}
|
||||
|
||||
protected async _strings(locale: string) {
|
||||
const response = await fetch(`/api/strings/${locale}.json`);
|
||||
const json = (await response.json()) as Strings;
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@@ -101,17 +101,14 @@ export class ServerAPI extends AbstractAPI {
|
||||
return strings;
|
||||
}
|
||||
|
||||
protected async _groupedGameStrings(
|
||||
locale: string,
|
||||
group: string,
|
||||
prefix: boolean
|
||||
) {
|
||||
protected async _groupedGameStrings(locale: string, group: string) {
|
||||
const strings = await this.gameStrings(locale);
|
||||
const filtered: Record<string, string> = {};
|
||||
|
||||
for (const key in strings) {
|
||||
if (key.startsWith(`${group}__`)) {
|
||||
filtered[prefix ? key : key.substring(group.length + 2)] = strings[key];
|
||||
const trimmed = key.substring(group.length + 2);
|
||||
filtered[trimmed] = strings[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +130,7 @@ export class ServerAPI extends AbstractAPI {
|
||||
protected async _tankList() {
|
||||
const group = this.metadata.group("TankEntity");
|
||||
const list: TankListEntry[] = [];
|
||||
const strings = await this.groupedGameStrings("en", "TankEntity");
|
||||
const strings = await this.groupedGameStrings("en", "TankEntity", true);
|
||||
|
||||
for (const item of group) {
|
||||
const tankCatalog = item.TankCatalog();
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface Tool {
|
||||
significant?: boolean;
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
|
||||
background_position: "top" | "center" | "bottom";
|
||||
}
|
||||
|
||||
export const tools: Record<string, Tool> = {
|
||||
@@ -16,11 +18,15 @@ export const tools: Record<string, Tool> = {
|
||||
strings: "tankopedia",
|
||||
button: "purple",
|
||||
significant: true,
|
||||
|
||||
background_position: "top",
|
||||
},
|
||||
|
||||
avatars: {
|
||||
id: "avatars",
|
||||
button: "gold",
|
||||
|
||||
background_position: "bottom",
|
||||
},
|
||||
|
||||
backgrounds: {
|
||||
|
||||
@@ -3,11 +3,10 @@ import { useAwait } from "./useAwait";
|
||||
import { useLocale } from "./useLocale";
|
||||
|
||||
export function useGameStrings(group: string) {
|
||||
const { locale } = useLocale();
|
||||
const locale = useLocale();
|
||||
const strings = useAwait(
|
||||
() => api.groupedGameStrings(locale, group),
|
||||
() => api.groupedGameStrings(locale, group, true),
|
||||
`game-strings-${locale}-${group}`
|
||||
);
|
||||
|
||||
return strings;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import { createContext, useContext, type ReactNode } from "react";
|
||||
import { api } from "../core/api/dynamic";
|
||||
import { useAwait } from "./useAwait";
|
||||
|
||||
const LocaleContext = createContext<{
|
||||
locale: string;
|
||||
@@ -26,12 +24,7 @@ export function useLocale() {
|
||||
throw new Error("useLocale must be used within a LocaleProvider");
|
||||
}
|
||||
|
||||
const strings = useAwait(
|
||||
() => api.strings(context.locale),
|
||||
`strings-${context.locale}`
|
||||
);
|
||||
|
||||
return { locale: context.locale, strings };
|
||||
return context.locale;
|
||||
}
|
||||
|
||||
export interface LocaleAcceptorProps {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { api } from "../core/api/dynamic";
|
||||
import { useAwait } from "./useAwait";
|
||||
import { useLocale } from "./useLocale";
|
||||
|
||||
export function useStrings() {
|
||||
const locale = useLocale();
|
||||
const strings = useAwait(() => api.strings(locale), `strings-${locale}`);
|
||||
return strings;
|
||||
}
|
||||
@@ -16,12 +16,14 @@ interface Props {
|
||||
const { accent, noNavbar, locale } = Astro.props;
|
||||
---
|
||||
|
||||
<html>
|
||||
<html lang={locale}>
|
||||
<head>
|
||||
<BaseMeta />
|
||||
|
||||
<ColorScale color="mauve" />
|
||||
<ColorScale color="mauve" alpha />
|
||||
<ColorAlias color="mauve" alias="gray" />
|
||||
<ColorAlias color="mauve" alpha alias="gray" />
|
||||
|
||||
<ColorScale color="black" alpha />
|
||||
<ColorScale color="white" alpha />
|
||||
@@ -35,12 +37,19 @@ const { accent, noNavbar, locale } = Astro.props;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
min-height: 100dvh;
|
||||
|
||||
background-color: var(--gray-1);
|
||||
color: var(--gray-12);
|
||||
font-family: var(--font-family-regular);
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-4) 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +59,11 @@ const { accent, noNavbar, locale } = Astro.props;
|
||||
<body class="dark">
|
||||
{!noNavbar && <Navbar {locale} />}
|
||||
|
||||
<slot />
|
||||
<slot name="pre-app" />
|
||||
|
||||
<div class="app">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<Footer {locale} />
|
||||
</body>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
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 { LocaleProvider } from "../../../hooks/useLocale";
|
||||
import { useGameStrings } from "../../../hooks/useGameStrings";
|
||||
import { LocaleProvider, useLocale } from "../../../hooks/useLocale";
|
||||
import type { Avatar } from "../../../protos/avatar";
|
||||
|
||||
interface PageProps extends ContentProps {
|
||||
locale: string;
|
||||
@@ -20,13 +24,46 @@ interface ContentProps {
|
||||
}
|
||||
|
||||
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;
|
||||
}),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="avatars">
|
||||
{avatars.slice(0, 12).map((avatar) => (
|
||||
<AvatarCard key={avatar.name} avatar={avatar} />
|
||||
))}
|
||||
<IncrementalLoader
|
||||
initial={40}
|
||||
intermediate={10}
|
||||
data={ordered}
|
||||
Component={AvatarCard}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
import Section from "../../../components/Section.astro";
|
||||
import ToolBanner from "../../../components/ToolBanner.astro";
|
||||
import { tools } from "../../../core/tools/tools";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
@@ -17,6 +19,8 @@ const { locale } = Astro.props;
|
||||
</style>
|
||||
|
||||
<BaseLayout {locale} accent="gold">
|
||||
<ToolBanner {locale} tool={tools.avatars} slot="pre-app" />
|
||||
|
||||
<Section>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type SkeletonProps<Props> =
|
||||
| { skeleton: true }
|
||||
| (Props & { skeleton?: false });
|
||||
Reference in new issue
Block a user