mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
feat: skeleton tankopedia page
This commit is contained in:
1 parent
0722ff6bfb
commit
cb45faf83c
6 files changed
+41
-21
No files matched your search
@@ -6,6 +6,7 @@ import { NAVBAR_HEIGHT } from '../../../constants/navbar';
|
|||||||
import { Var } from '../../../core/radix/var';
|
import { Var } from '../../../core/radix/var';
|
||||||
import { useFullScreen } from '../../../hooks/useFullScreen';
|
import { useFullScreen } from '../../../hooks/useFullScreen';
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from '../../../stores/duel';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||||
import type { ThicknessRange } from '../../Armor/components/StaticArmor';
|
import type { ThicknessRange } from '../../Armor/components/StaticArmor';
|
||||||
import { classIcons } from '../../ClassIcon';
|
import { classIcons } from '../../ClassIcon';
|
||||||
import { Options } from './components/Options';
|
import { Options } from './components/Options';
|
||||||
@@ -14,7 +15,7 @@ import { TankSandboxLoader } from './components/TankSandboxLoader';
|
|||||||
|
|
||||||
const tankDefinitions = await fetchTankDefinitions();
|
const tankDefinitions = await fetchTankDefinitions();
|
||||||
|
|
||||||
export function HeroSection() {
|
export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const canvas = useRef<HTMLCanvasElement>(null);
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
const isFullScreen = useFullScreen();
|
const isFullScreen = useFullScreen();
|
||||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||||
@@ -124,6 +125,8 @@ export function HeroSection() {
|
|||||||
>
|
>
|
||||||
<Box position="absolute" width="100%" height="100%">
|
<Box position="absolute" width="100%" height="100%">
|
||||||
<Box width="100%" height="100%">
|
<Box width="100%" height="100%">
|
||||||
|
{skeleton && <TankSandboxLoader id={protagonist.id} />}
|
||||||
|
|
||||||
<Suspense fallback={<TankSandboxLoader id={protagonist.id} />}>
|
<Suspense fallback={<TankSandboxLoader id={protagonist.id} />}>
|
||||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -5,19 +5,20 @@ import {
|
|||||||
formatCompact,
|
formatCompact,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { Box, Flex, Link, Text } from '@radix-ui/themes';
|
import { Box, Flex, Link, Skeleton, Text } from '@radix-ui/themes';
|
||||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps';
|
||||||
|
|
||||||
interface NodeProps {
|
type NodeProps = MaybeSkeletonComponentProps & {
|
||||||
id: number;
|
id: number;
|
||||||
nextIds?: number[];
|
nextIds?: number[];
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
const tankDefinitions = await fetchTankDefinitions();
|
const tankDefinitions = await fetchTankDefinitions();
|
||||||
const averageDefinitions = await fetchAverageDefinitions();
|
const averageDefinitions = await fetchAverageDefinitions();
|
||||||
|
|
||||||
export function Node({ id, highlight, nextIds }: NodeProps) {
|
export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||||
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
||||||
const tank = tankDefinitions.tanks[id];
|
const tank = tankDefinitions.tanks[id];
|
||||||
const nextTanks = nextIds?.map((id) => tankDefinitions.tanks[id]);
|
const nextTanks = nextIds?.map((id) => tankDefinitions.tanks[id]);
|
||||||
@@ -125,7 +126,12 @@ export function Node({ id, highlight, nextIds }: NodeProps) {
|
|||||||
|
|
||||||
{averages && nextTanks && (
|
{averages && nextTanks && (
|
||||||
<Text color="gray" size="1" mt="2">
|
<Text color="gray" size="1" mt="2">
|
||||||
{games!} {games === 1 ? 'battle' : 'battles'}
|
{skeleton && <Skeleton height="1em" width="4em" />}
|
||||||
|
{!skeleton && (
|
||||||
|
<>
|
||||||
|
{games!} {games === 1 ? 'battle' : 'battles'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Flex, Heading, IconButton, ScrollArea, Text } from '@radix-ui/themes';
|
|||||||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from '../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||||
import { Arrow } from './components/Arrow';
|
import { Arrow } from './components/Arrow';
|
||||||
import { Node } from './components/Node';
|
import { Node } from './components/Node';
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ const tankDefinitions = await fetchTankDefinitions();
|
|||||||
|
|
||||||
export const XP_MULTIPLIERS = [1, 2, 3, 4, 5, 10];
|
export const XP_MULTIPLIERS = [1, 2, 3, 4, 5, 10];
|
||||||
|
|
||||||
export function TechTreeSection() {
|
export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const master = Duel.use((state) => state.protagonist.tank);
|
const master = Duel.use((state) => state.protagonist.tank);
|
||||||
|
|
||||||
if (master.type !== TankType.RESEARCHABLE || master.deprecated) return null;
|
if (master.type !== TankType.RESEARCHABLE || master.deprecated) return null;
|
||||||
@@ -184,6 +185,7 @@ export function TechTreeSection() {
|
|||||||
id={id}
|
id={id}
|
||||||
nextIds={last ? tank.successors : [line[index + 1]]}
|
nextIds={last ? tank.successors : [line[index + 1]]}
|
||||||
highlight={last}
|
highlight={last}
|
||||||
|
skeleton={skeleton}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@@ -199,7 +201,7 @@ export function TechTreeSection() {
|
|||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Node key={`${id}-node}`} id={id} />
|
<Node key={`${id}-node}`} id={id} skeleton={skeleton} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { fetchReviews, youtubers } from '@blitzkit/core';
|
import { fetchReviews, youtubers } from '@blitzkit/core';
|
||||||
import { Box, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
import { Box, Flex, Heading, Link, Skeleton, Text } from '@radix-ui/themes';
|
||||||
import { Var } from '../../core/radix/var';
|
import { Var } from '../../core/radix/var';
|
||||||
import { Duel } from '../../stores/duel';
|
import { Duel } from '../../stores/duel';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||||
import { PageWrapper } from '../PageWrapper';
|
import { PageWrapper } from '../PageWrapper';
|
||||||
import { VerifiedIcon } from '../VerifiedIcon';
|
import { VerifiedIcon } from '../VerifiedIcon';
|
||||||
|
|
||||||
const reviews = await fetchReviews();
|
const reviews = await fetchReviews();
|
||||||
|
|
||||||
export function VideoSection() {
|
export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const videos = reviews.reviews[tank.id]?.videos;
|
const videos = reviews.reviews[tank.id]?.videos;
|
||||||
|
|
||||||
@@ -45,14 +46,16 @@ export function VideoSection() {
|
|||||||
style={{
|
style={{
|
||||||
borderRadius: 'var(--radius-3)',
|
borderRadius: 'var(--radius-3)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
background: `url(${image})`,
|
background: skeleton
|
||||||
|
? Var('color-panel-solid')
|
||||||
|
: `url(${image})`,
|
||||||
backgroundPosition: '0 18px',
|
backgroundPosition: '0 18px',
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
background: `url(${image})`,
|
background: skeleton ? 'transparent' : `url(${image})`,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
height: 128,
|
height: 128,
|
||||||
@@ -70,8 +73,11 @@ export function VideoSection() {
|
|||||||
align="center"
|
align="center"
|
||||||
gap="1"
|
gap="1"
|
||||||
>
|
>
|
||||||
<Text>{youtuber.name}</Text>
|
<Text>
|
||||||
<VerifiedIcon width="1em" height="1em" />
|
{!skeleton && youtuber.name}
|
||||||
|
{skeleton && <Skeleton height="1em" width="7rem" />}
|
||||||
|
</Text>
|
||||||
|
{!skeleton && <VerifiedIcon width="1em" height="1em" />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -17,16 +17,17 @@ import { App } from '../../../../stores/app';
|
|||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from '../../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||||
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps';
|
||||||
|
|
||||||
interface PageProps {
|
type PageProps = MaybeSkeletonComponentProps & {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
const tankDefinitions = await fetchTankDefinitions();
|
const tankDefinitions = await fetchTankDefinitions();
|
||||||
const provisionDefinitions = await fetchProvisionDefinitions();
|
const provisionDefinitions = await fetchProvisionDefinitions();
|
||||||
const modelDefinitions = await fetchModelDefinitions();
|
const modelDefinitions = await fetchModelDefinitions();
|
||||||
|
|
||||||
export function Page({ id }: PageProps) {
|
export function Page({ id, skeleton }: PageProps) {
|
||||||
const tank = tankDefinitions.tanks[id];
|
const tank = tankDefinitions.tanks[id];
|
||||||
const model = modelDefinitions.models[id];
|
const model = modelDefinitions.models[id];
|
||||||
|
|
||||||
@@ -45,14 +46,14 @@ export function Page({ id }: PageProps) {
|
|||||||
gap="9"
|
gap="9"
|
||||||
pb="9"
|
pb="9"
|
||||||
>
|
>
|
||||||
<HeroSection />
|
<HeroSection skeleton={skeleton} />
|
||||||
<ShotDisplaySection />
|
<ShotDisplaySection />
|
||||||
<CalloutsSection />
|
<CalloutsSection />
|
||||||
<MetaSection />
|
<MetaSection />
|
||||||
<TechTreeSection />
|
<TechTreeSection skeleton={skeleton} />
|
||||||
<CharacteristicsSection />
|
<CharacteristicsSection />
|
||||||
<GameModeSection />
|
<GameModeSection />
|
||||||
<VideoSection />
|
<VideoSection skeleton={skeleton} />
|
||||||
<HistorySection />
|
<HistorySection />
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</Duel.Provider>
|
</Duel.Provider>
|
||||||
|
|||||||
@@ -30,5 +30,7 @@ const tank = tankDefinitions.tanks[id];
|
|||||||
</title>
|
</title>
|
||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<Page id={id} client:load />
|
<Page id={id} client:only="react">
|
||||||
|
<Page id={id} skeleton slot="fallback" />
|
||||||
|
</Page>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
Reference in new issue
Block a user