mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: migration 3
This commit is contained in:
1 parent
6285f33989
commit
4055988478
110 files changed
+28
-76
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
export interface GlowProps extends ComponentProps<'div'> {
|
||||
color: string;
|
||||
direction?: 'default' | 'reverse';
|
||||
rotation?: number;
|
||||
startOpacity?: number;
|
||||
endOpacity?: number;
|
||||
}
|
||||
|
||||
const width = 16;
|
||||
|
||||
export function Glow({
|
||||
color,
|
||||
direction = 'default',
|
||||
rotation = 0,
|
||||
style,
|
||||
startOpacity = 0,
|
||||
endOpacity = 1,
|
||||
...props
|
||||
}: GlowProps) {
|
||||
const color1 = `${color}${Math.round(endOpacity * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')}`;
|
||||
const color2 = `${color}${Math.round(startOpacity * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')}`;
|
||||
const background = `linear-gradient(${
|
||||
direction === 'default' ? 0 : 180
|
||||
}deg, ${color1} 0%, ${color2} 100%)`;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: direction === 'default' ? 'flex-end' : 'flex-start',
|
||||
justifyContent: 'center',
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<div style={{ height: 54, width, background }} />
|
||||
<div style={{ height: 72, width, background }} />
|
||||
<div style={{ height: 80, width, background }} />
|
||||
<div style={{ height: 72, width, background }} />
|
||||
<div style={{ height: 54, width, background }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { theme } from '../../../../stitches.config';
|
||||
import { Glow } from './components/Glow';
|
||||
|
||||
export interface WN8DisplayProps {
|
||||
subtitle: string;
|
||||
color: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export function HeroStat({ color, subtitle, value }: WN8DisplayProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 128 * 2, // idk why twice the width turns into the correct width
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Glow color={color} style={{ marginBottom: -16 }} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: 128,
|
||||
height: 80,
|
||||
backgroundColor: color,
|
||||
borderRadius: 40,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'block',
|
||||
fontWeight: 900,
|
||||
fontSize: 32,
|
||||
color: theme.colors.textHighContrast,
|
||||
lineHeight: '26px',
|
||||
letterSpacing: '-0.03em',
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 16,
|
||||
lineHeight: '16px',
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Glow color={color} direction="reverse" style={{ marginTop: -16 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export enum WN8SurroundingStatAlign {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
export interface WN8SurroundingStatProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
align?: WN8SurroundingStatAlign;
|
||||
padded?: boolean;
|
||||
}
|
||||
|
||||
export function WN8SurroundingStat({
|
||||
label,
|
||||
value,
|
||||
align = WN8SurroundingStatAlign.Left,
|
||||
padded = false,
|
||||
}: WN8SurroundingStatProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems:
|
||||
align === WN8SurroundingStatAlign.Left ? 'flex-start' : 'flex-end',
|
||||
paddingLeft: padded && align === WN8SurroundingStatAlign.Left ? 32 : 0,
|
||||
paddingRight:
|
||||
padded && align === WN8SurroundingStatAlign.Right ? 32 : 0,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Percentile } from '@blitzkit/core/src/statistics/percentiles';
|
||||
import { Locale } from 'discord.js';
|
||||
import { SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
||||
import { translator } from '../../core/localization/translator';
|
||||
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
|
||||
import { PERCENTILE_COLORS } from '../PercentileIndicator/constants';
|
||||
import { HeroStat } from './components/HeroStat';
|
||||
import {
|
||||
WN8SurroundingStat,
|
||||
WN8SurroundingStatAlign,
|
||||
} from './components/WN8SurroundingStat';
|
||||
|
||||
export interface AllStatsOverviewProps {
|
||||
stats: {
|
||||
battles: number;
|
||||
wins: number;
|
||||
damage_dealt: number;
|
||||
damage_received: number;
|
||||
survived_battles: number;
|
||||
hits: number;
|
||||
shots: number;
|
||||
frags: number;
|
||||
};
|
||||
supplementaryStats: SupplementaryStats;
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
export default function AllStatsOverview({
|
||||
stats,
|
||||
locale,
|
||||
supplementaryStats,
|
||||
}: AllStatsOverviewProps) {
|
||||
const { t, translate } = translator(locale);
|
||||
const percentile =
|
||||
typeof supplementaryStats.WN8 === 'number'
|
||||
? getWN8Percentile(supplementaryStats.WN8)
|
||||
: Percentile.VeryBad;
|
||||
const color = PERCENTILE_COLORS[percentile];
|
||||
|
||||
const heroStat =
|
||||
typeof supplementaryStats.WN8 === 'number'
|
||||
? supplementaryStats.WN8.toFixed(0)
|
||||
: '--';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: -64,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
alignItems: 'flex-end',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<WN8SurroundingStat
|
||||
label={t`bot.common.hero_stats.battles`}
|
||||
value={stats.battles.toLocaleString()}
|
||||
align={WN8SurroundingStatAlign.Right}
|
||||
/>
|
||||
<WN8SurroundingStat
|
||||
label={t`bot.common.hero_stats.winrate`}
|
||||
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
|
||||
align={WN8SurroundingStatAlign.Right}
|
||||
padded
|
||||
/>
|
||||
<WN8SurroundingStat
|
||||
label={t`bot.common.hero_stats.damage`}
|
||||
value={Math.round(
|
||||
stats.damage_dealt / stats.battles,
|
||||
).toLocaleString()}
|
||||
align={WN8SurroundingStatAlign.Right}
|
||||
padded
|
||||
/>
|
||||
<WN8SurroundingStat
|
||||
label={t`bot.common.hero_stats.damage_ratio`}
|
||||
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
|
||||
align={WN8SurroundingStatAlign.Right}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HeroStat
|
||||
value={heroStat}
|
||||
subtitle={translate(`common.wn8_percentile.${percentile}`)}
|
||||
color={color}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<WN8SurroundingStat
|
||||
label={t`bot.common.hero_stats.tier`}
|
||||
value={
|
||||
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
|
||||
}
|
||||
/>
|
||||
<WN8SurroundingStat
|
||||
label={t`bot.common.hero_stats.survival`}
|
||||
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
|
||||
2,
|
||||
)}%`}
|
||||
padded
|
||||
/>
|
||||
<WN8SurroundingStat
|
||||
label={t`bot.common.hero_stats.accuracy`}
|
||||
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
|
||||
padded
|
||||
/>
|
||||
<WN8SurroundingStat
|
||||
label={t`bot.common.hero_stats.kills`}
|
||||
value={(stats.frags / stats.battles).toFixed(2)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface RootProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div
|
||||
className="session-tracker-root"
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 8 }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Percentile } from '@blitzkit/core/src/statistics/percentiles';
|
||||
import { createColors } from 'bepaint';
|
||||
import {
|
||||
AccentColor,
|
||||
GrayColor,
|
||||
PALETTES,
|
||||
} from '../../../constants/radixColors';
|
||||
import { TankClass, TreeType } from '../../Tanks';
|
||||
import { TREE_TYPE_ICONS } from '../../Tanks/components/Item/constants';
|
||||
import { RowStat } from './RowStat';
|
||||
|
||||
export interface RowStatItem {
|
||||
title: string;
|
||||
current?: number | string;
|
||||
career?: number | string;
|
||||
delta?: number;
|
||||
percentile?: Percentile;
|
||||
}
|
||||
|
||||
interface RowProps {
|
||||
type?: 'tank' | 'summary';
|
||||
naked?: boolean;
|
||||
title: string;
|
||||
minimized?: boolean;
|
||||
treeType?: TreeType;
|
||||
tankType?: TankClass;
|
||||
color?: AccentColor | GrayColor;
|
||||
stats: (RowStatItem | undefined)[];
|
||||
}
|
||||
|
||||
export function Row({
|
||||
color = 'slate',
|
||||
stats,
|
||||
title,
|
||||
minimized,
|
||||
tankType,
|
||||
treeType,
|
||||
naked = false,
|
||||
type,
|
||||
}: RowProps) {
|
||||
const palette = PALETTES[`${color}Dark`];
|
||||
const theme = createColors(palette);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="session-tracker-row"
|
||||
style={{
|
||||
maxWidth: naked ? 600 : Infinity,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 4,
|
||||
backgroundColor: naked
|
||||
? `${theme.appBackground2}c0`
|
||||
: theme.appBackground2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 8,
|
||||
gap: 4,
|
||||
backgroundColor: naked
|
||||
? `${theme.componentInteractive}c0`
|
||||
: theme.componentInteractive,
|
||||
}}
|
||||
>
|
||||
{type === 'tank' && tankType !== undefined && (
|
||||
<img
|
||||
alt={tankType}
|
||||
src={TREE_TYPE_ICONS[treeType ?? 'researchable'][tankType]}
|
||||
style={{ width: 16, height: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
type === 'tank'
|
||||
? treeType === 'collector'
|
||||
? theme.textLowContrast_blue
|
||||
: treeType === 'premium'
|
||||
? theme.textLowContrast_amber
|
||||
: theme.textHighContrast
|
||||
: theme.textLowContrast,
|
||||
fontWeight: 900,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
{stats.map(
|
||||
(row, index) =>
|
||||
row && (
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
key={index}
|
||||
name={`${row.title} • ${row.career ?? '--'}`}
|
||||
value={row.current ?? '--'}
|
||||
delta={row.delta}
|
||||
percentile={row.percentile}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Percentile } from '@blitzkit/core/src/statistics/percentiles';
|
||||
import { createColors } from 'bepaint';
|
||||
import {
|
||||
AccentColor,
|
||||
GrayColor,
|
||||
PALETTES,
|
||||
} from '../../../constants/radixColors';
|
||||
import PercentileIndicator from '../../PercentileIndicator';
|
||||
|
||||
interface RowStatProps {
|
||||
name: string;
|
||||
value?: string | number;
|
||||
delta?: number;
|
||||
percentile?: Percentile;
|
||||
minimized?: boolean;
|
||||
color?: AccentColor | GrayColor;
|
||||
}
|
||||
|
||||
export function RowStat({
|
||||
name,
|
||||
value,
|
||||
delta,
|
||||
percentile,
|
||||
minimized = false,
|
||||
color = 'slate',
|
||||
}: RowStatProps) {
|
||||
const theme = createColors(PALETTES[`${color}Dark`]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="session-tracker-row-stat"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{percentile === undefined && delta !== undefined && delta !== 0 && (
|
||||
<img
|
||||
alt={delta > 0 ? 'Increase' : 'Decrease'}
|
||||
src={
|
||||
(delta ?? 0) > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
style={{ width: 12, height: 12 }}
|
||||
/>
|
||||
)}
|
||||
{percentile !== undefined && (
|
||||
<PercentileIndicator percentile={percentile} />
|
||||
)}
|
||||
|
||||
<span
|
||||
style={{
|
||||
color: theme.textHighContrast,
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!minimized && (
|
||||
<span
|
||||
style={{
|
||||
color: theme.textLowContrast,
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './components/Root';
|
||||
export * from './components/Row';
|
||||
export * from './components/RowStat';
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
interface CommandWrapperProps {
|
||||
children: ReactNode;
|
||||
fat?: boolean;
|
||||
}
|
||||
|
||||
export default function CommandWrapper({ children, fat }: CommandWrapperProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: `16px ${fat ? 0 : 16}px`,
|
||||
gap: 16,
|
||||
width: '100%',
|
||||
color: theme.colors.textHighContrast,
|
||||
position: 'relative',
|
||||
background: theme.colors.appBackground1,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
interface DeltaCaretProps {
|
||||
delta: number;
|
||||
}
|
||||
|
||||
export function DeltaCaret({ delta }: DeltaCaretProps) {
|
||||
return (
|
||||
<img
|
||||
alt={delta > 0 ? 'Increase' : 'Decrease'}
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
src={
|
||||
delta > 0
|
||||
? 'https://i.imgur.com/gjI4a5f.png'
|
||||
: 'https://i.imgur.com/lLGZygN.png'
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { asset } from '@blitzkit/core/src/blitzkit/asset';
|
||||
|
||||
interface FlagProps {
|
||||
nation: string;
|
||||
}
|
||||
|
||||
export function Flag({ nation }: FlagProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<img alt={nation} src={asset(`flags/circle/${nation}.webp`)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Percentile } from '@blitzkit/core/src/statistics/percentiles';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import PercentileIndicator from '../../PercentileIndicator';
|
||||
|
||||
interface CardItem {
|
||||
label: string;
|
||||
value: string | number | undefined;
|
||||
percentile?: Percentile;
|
||||
}
|
||||
|
||||
interface CardProps {
|
||||
items: CardItem[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function Card({ items, title }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
flex: 1,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.colors.componentInteractive,
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
{item.label}
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 1,
|
||||
borderRadius: 0.5,
|
||||
backgroundColor: theme.colors.borderNonInteractive,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{item.percentile !== undefined && (
|
||||
<PercentileIndicator percentile={item.percentile} />
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
{item.value === undefined || Number.isNaN(item.value)
|
||||
? '--'
|
||||
: item.value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
interface RootProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
interface RowProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Row({ children }: RowProps) {
|
||||
return <div style={{ display: 'flex', gap: 8 }}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import isNumber from '@blitzkit/core/src/math/isNumber';
|
||||
import { Locale } from 'discord.js';
|
||||
import { SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
||||
import { translator } from '../../core/localization/translator';
|
||||
import { BlitzStats } from '../../core/statistics/compositeStats/constants';
|
||||
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
|
||||
import { Card } from './components/Card';
|
||||
import { Root } from './components/Root';
|
||||
import { Row } from './components/Row';
|
||||
|
||||
interface GenericAllStatsProps {
|
||||
stats: BlitzStats;
|
||||
supplementaryStats?: SupplementaryStats;
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
export default function GenericAllStats({
|
||||
stats,
|
||||
supplementaryStats,
|
||||
locale,
|
||||
}: GenericAllStatsProps) {
|
||||
const { t } = translator(locale);
|
||||
const damageRatio = stats.damage_dealt / stats.damage_received;
|
||||
const killsToDeathRatio =
|
||||
stats.frags / (stats.battles - stats.survived_battles);
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<Row>
|
||||
<Card
|
||||
title={t`bot.commands.full_stats.body.cards.games`}
|
||||
items={[
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.games.winrate`,
|
||||
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.games.battles`,
|
||||
value: stats.battles,
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.games.wins_losses`,
|
||||
value: `${stats.wins} / ${stats.losses}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
title={t`bot.commands.full_stats.body.cards.efficiency`}
|
||||
items={[
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.efficiency.wn8`,
|
||||
value:
|
||||
supplementaryStats && isNumber(supplementaryStats.WN8)
|
||||
? supplementaryStats.WN8!.toFixed(0)
|
||||
: undefined,
|
||||
percentile: isNumber(supplementaryStats?.WN8)
|
||||
? getWN8Percentile(supplementaryStats!.WN8!)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.efficiency.survival`,
|
||||
value: `${(
|
||||
100 *
|
||||
(stats.survived_battles / stats.battles)
|
||||
).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.efficiency.accuracy`,
|
||||
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Card
|
||||
title={t`bot.commands.full_stats.body.cards.lethality`}
|
||||
items={[
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.lethality.average_damage`,
|
||||
value: (stats.damage_dealt / stats.battles).toFixed(0),
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.lethality.damage_per_tier`,
|
||||
value: supplementaryStats?.tier
|
||||
? (
|
||||
stats.damage_dealt /
|
||||
stats.battles /
|
||||
supplementaryStats.tier
|
||||
).toFixed(0)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.lethality.damage_ratio`,
|
||||
value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined,
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.lethality.average_kills`,
|
||||
value: (stats.frags / stats.battles).toFixed(0),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
title={t`bot.commands.full_stats.body.cards.miscellaneous`}
|
||||
items={[
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.miscellaneous.average_tier`,
|
||||
value: supplementaryStats?.tier?.toFixed(2),
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.miscellaneous.average_spots`,
|
||||
value: (stats.spotted / stats.battles).toFixed(2),
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.miscellaneous.average_xp`,
|
||||
value: (stats.xp / stats.battles).toFixed(0),
|
||||
},
|
||||
{
|
||||
label: t`bot.commands.full_stats.body.cards.miscellaneous.kills_to_death_ratio`,
|
||||
value: isFinite(killsToDeathRatio)
|
||||
? killsToDeathRatio.toFixed(2)
|
||||
: undefined,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Row>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { GenericStat } from '..';
|
||||
import GenericStatsRow from './GenericStatsRow';
|
||||
|
||||
interface GenericStatsColumnProps {
|
||||
stats: GenericStat[];
|
||||
}
|
||||
|
||||
export default function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
|
||||
return (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{stats.map((stat) => (
|
||||
<GenericStatsRow key={stat[0]} stat={stat} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { GenericStat } from '..';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import PercentileIndicator from '../../PercentileIndicator';
|
||||
|
||||
interface GenericStatsRowProps {
|
||||
stat: GenericStat;
|
||||
}
|
||||
|
||||
export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: 128,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{stat[0]}
|
||||
</span>
|
||||
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{stat[2] !== undefined && <PercentileIndicator percentile={stat[2]} />}
|
||||
{stat[1]}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Percentile } from '@blitzkit/core/src/statistics/percentiles';
|
||||
import GenericStatsColumn from './components/GenericStatsColumn';
|
||||
|
||||
export type GenericStat =
|
||||
| [string, string | number | undefined]
|
||||
| [string, string | number | undefined, Percentile | undefined];
|
||||
|
||||
export interface GenericStatsProps {
|
||||
stats: GenericStat[];
|
||||
columnCount?: number;
|
||||
}
|
||||
|
||||
export default function GenericStats({
|
||||
stats,
|
||||
columnCount = 2,
|
||||
}: GenericStatsProps) {
|
||||
const filteredStats = stats.filter(([, value]) => value !== undefined);
|
||||
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
|
||||
const columns: GenericStat[][] = [];
|
||||
|
||||
filteredStats.forEach((stat, index) => {
|
||||
const column = Math.floor(index / itemsPerRow);
|
||||
if (!columns[column]) columns[column] = [];
|
||||
columns[column].push(stat);
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 32 }}>
|
||||
{columns.map((stats) => (
|
||||
<GenericStatsColumn
|
||||
key={stats.map((stat) => stat[0]).join('-')}
|
||||
stats={stats}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum LineColor {
|
||||
Red,
|
||||
Green,
|
||||
Blue,
|
||||
White,
|
||||
}
|
||||
export enum LineStyle {
|
||||
Solid,
|
||||
Dashes,
|
||||
DashesAndDots,
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { blue, green, red, slateDark } from '@radix-ui/colors';
|
||||
import { GRAPH_HEIGHT, GRAPH_WIDTH } from '../Root/constants';
|
||||
import { LineColor, LineStyle } from './constants';
|
||||
|
||||
const LINE_COLORS = {
|
||||
[LineColor.Red]: red.red9,
|
||||
[LineColor.Green]: green.green9,
|
||||
[LineColor.Blue]: blue.blue9,
|
||||
[LineColor.White]: slateDark.slate11,
|
||||
};
|
||||
|
||||
const LINE_STYLES = {
|
||||
[LineStyle.Solid]: undefined,
|
||||
[LineStyle.Dashes]: '8, 8',
|
||||
[LineStyle.DashesAndDots]: '1, 4, 8, 4',
|
||||
};
|
||||
|
||||
export type PlotItem = [number, number];
|
||||
type Plot = PlotItem[];
|
||||
|
||||
interface LineProps {
|
||||
plot: Plot;
|
||||
minY?: number;
|
||||
maxY?: number;
|
||||
color?: LineColor;
|
||||
style?: LineStyle;
|
||||
lineWidth?: number;
|
||||
}
|
||||
|
||||
export function Line({
|
||||
plot,
|
||||
minY: providedMinY,
|
||||
maxY: providedMaxY,
|
||||
color = LineColor.Red,
|
||||
style = LineStyle.Solid,
|
||||
lineWidth = 2,
|
||||
}: LineProps) {
|
||||
const xs = plot.map(([x]) => x);
|
||||
const minX = Math.min(...xs);
|
||||
const maxX = Math.max(...xs);
|
||||
const ys = plot.map(([, y]) => y);
|
||||
const minY = providedMinY ?? Math.min(...ys);
|
||||
const maxY = providedMaxY ?? Math.max(...ys);
|
||||
const rangeX = maxX - minX;
|
||||
const rangeY = maxY - minY;
|
||||
const points = plot
|
||||
.map(
|
||||
([x, y]) =>
|
||||
`${(x - minX) * (GRAPH_WIDTH / rangeX)},${
|
||||
GRAPH_HEIGHT - (y - minY) * (GRAPH_HEIGHT / rangeY)
|
||||
}`,
|
||||
)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<polyline
|
||||
vector-effect="non-scaling-stroke"
|
||||
fill="none"
|
||||
stroke={LINE_COLORS[color]}
|
||||
stroke-width={lineWidth}
|
||||
stroke-dasharray={LINE_STYLES[style]}
|
||||
points={points}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum MarginOrientation {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { times } from 'lodash';
|
||||
import { theme } from '../../../../stitches.config';
|
||||
import { MarginInputProps } from '../Root';
|
||||
import { MarginOrientation } from './constants';
|
||||
|
||||
interface MarginProps extends MarginInputProps {
|
||||
verticalSeparations: number;
|
||||
orientation?: MarginOrientation;
|
||||
}
|
||||
|
||||
export function Margin({
|
||||
verticalSeparations,
|
||||
max,
|
||||
min,
|
||||
suffix,
|
||||
precision = 0,
|
||||
orientation = MarginOrientation.Vertical,
|
||||
}: MarginProps) {
|
||||
const isVertical = orientation === MarginOrientation.Vertical;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: isVertical ? 0 : 64,
|
||||
width: isVertical ? 48 : '100%',
|
||||
display: 'flex',
|
||||
flexDirection: isVertical ? 'column' : 'row-reverse',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{times(verticalSeparations, (index) => (
|
||||
<span
|
||||
key={index}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
{`${(
|
||||
(max - min) *
|
||||
((verticalSeparations - (index + 1)) /
|
||||
(verticalSeparations - 1)) +
|
||||
min
|
||||
).toFixed(precision)}${suffix ?? ''}`}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const GRAPH_WIDTH = 384;
|
||||
export const GRAPH_HEIGHT = 320;
|
||||
@@ -0,0 +1,109 @@
|
||||
import { times } from 'lodash';
|
||||
import { ReactNode } from 'react';
|
||||
import { theme } from '../../../../stitches.config';
|
||||
import { Margin } from '../Margin';
|
||||
import { MarginOrientation } from '../Margin/constants';
|
||||
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './constants';
|
||||
|
||||
export interface MarginInputProps {
|
||||
min: number;
|
||||
max: number;
|
||||
suffix?: string;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
interface RootProps {
|
||||
children: ReactNode;
|
||||
verticalMargin?: MarginInputProps;
|
||||
horizontalMargin?: MarginInputProps;
|
||||
separations?: number;
|
||||
xMinLabel: string;
|
||||
xMaxLabel: string;
|
||||
xTitle?: string;
|
||||
}
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
verticalMargin,
|
||||
horizontalMargin,
|
||||
separations = 5,
|
||||
xMinLabel,
|
||||
xMaxLabel,
|
||||
xTitle,
|
||||
}: RootProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 64,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
|
||||
{xMinLabel}
|
||||
</span>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
|
||||
{xMaxLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
{verticalMargin && (
|
||||
<Margin {...verticalMargin} verticalSeparations={separations} />
|
||||
)}
|
||||
|
||||
<svg
|
||||
viewBox={`0 0 ${GRAPH_WIDTH} ${GRAPH_HEIGHT}`}
|
||||
preserveAspectRatio="none"
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.appBackground2,
|
||||
borderRadius: 4,
|
||||
border: theme.borderStyles.nonInteractive,
|
||||
}}
|
||||
>
|
||||
{times(separations - 2, (index) => {
|
||||
const y = GRAPH_HEIGHT * ((index + 1) / (separations - 1));
|
||||
|
||||
return (
|
||||
<polyline
|
||||
key={index}
|
||||
vector-effect="non-scaling-stroke"
|
||||
fill="none"
|
||||
stroke={theme.colors.borderNonInteractive}
|
||||
stroke-width="2"
|
||||
points={`0,${y}, ${GRAPH_WIDTH},${y}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{children}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{horizontalMargin && (
|
||||
<Margin
|
||||
{...horizontalMargin}
|
||||
verticalSeparations={separations}
|
||||
orientation={MarginOrientation.Horizontal}
|
||||
/>
|
||||
)}
|
||||
|
||||
{xTitle !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
{xTitle}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './components/Line';
|
||||
export * from './components/Margin';
|
||||
export * from './components/Root';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
interface GapProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function Gap({ message: number }: GapProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
backgroundColor: theme.colors.componentNonInteractive,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{number.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { ComponentProps } from 'react';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
interface ItemProps extends ComponentProps<'div'> {
|
||||
position: number;
|
||||
deltaPosition?: number;
|
||||
score?: number;
|
||||
deltaScore?: number;
|
||||
nickname: string;
|
||||
clan?: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
export function Item({
|
||||
position,
|
||||
deltaPosition,
|
||||
score,
|
||||
deltaScore,
|
||||
nickname,
|
||||
clan,
|
||||
highlight,
|
||||
...props
|
||||
}: ItemProps) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
backgroundColor: highlight
|
||||
? theme.colors.componentInteractive_blue
|
||||
: theme.colors.componentInteractive,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{position.toLocaleString()}.
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{nickname}
|
||||
</span>
|
||||
{clan && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
[{clan}]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{deltaPosition !== undefined && deltaPosition !== 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={deltaPosition > 0 ? 'Increase' : 'Decrease'}
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
}}
|
||||
src={
|
||||
deltaPosition > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{Math.abs(deltaPosition).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TODO: lol remove this spacer div for a better solution */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
width: 96,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{Math.abs(deltaScore).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||
<img
|
||||
alt={deltaScore > 0 ? 'Increase' : 'Decrease'}
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
}}
|
||||
src={
|
||||
deltaScore > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{score !== undefined && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{score.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface RootProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './components/Gap';
|
||||
export * from './components/Item';
|
||||
export * from './components/Root';
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum NoDataType {
|
||||
BattlesInPeriod,
|
||||
PlayersInPeriod,
|
||||
TanksFound,
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Locale } from 'discord.js';
|
||||
import { translator } from '../../core/localization/translator';
|
||||
import { theme } from '../../stitches.config';
|
||||
|
||||
interface NoDataProps {
|
||||
type:
|
||||
| 'battles_in_period'
|
||||
| 'players_in_period'
|
||||
| 'tanks_found'
|
||||
| 'battles_in_season';
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
export default function NoData({ type, locale }: NoDataProps) {
|
||||
const { translate } = translator(locale);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
{translate(`bot.common.no_data.${type}`)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Percentile } from '@blitzkit/core/src/statistics/percentiles';
|
||||
import {
|
||||
amber,
|
||||
blue,
|
||||
green,
|
||||
lime,
|
||||
orange,
|
||||
pink,
|
||||
purple,
|
||||
red,
|
||||
slate,
|
||||
teal,
|
||||
} from '@radix-ui/colors';
|
||||
|
||||
export const PERCENTILE_COLORS = {
|
||||
[Percentile.VeryBad]: slate.slate9,
|
||||
[Percentile.Bad]: red.red9,
|
||||
[Percentile.BelowAverage]: orange.orange9,
|
||||
[Percentile.Average]: amber.amber10,
|
||||
[Percentile.AboveAverage]: lime.lime8,
|
||||
[Percentile.Good]: green.green9,
|
||||
[Percentile.VeryGood]: teal.teal9,
|
||||
[Percentile.Great]: blue.blue9,
|
||||
[Percentile.Unicum]: pink.pink9,
|
||||
[Percentile.SuperUnicum]: purple.purple9,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Percentile } from '@blitzkit/core/src/statistics/percentiles';
|
||||
import { PERCENTILE_COLORS } from './constants';
|
||||
|
||||
interface PercentileIndicatorProps {
|
||||
percentile: Percentile;
|
||||
}
|
||||
|
||||
export default function PercentileIndicator({
|
||||
percentile,
|
||||
}: PercentileIndicatorProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: PERCENTILE_COLORS[percentile],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface ColumnProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Column({ children }: ColumnProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TankClass, TreeType } from '.';
|
||||
import {
|
||||
TANK_ICONS,
|
||||
TANK_ICONS_COLLECTOR,
|
||||
TANK_ICONS_PREMIUM,
|
||||
} from '../../../../core/blitzkit/tankDefinitions/constants';
|
||||
|
||||
export const TREE_TYPE_NAMES: Record<TreeType, string> = {
|
||||
researchable: 'Tech tree',
|
||||
premium: 'Premium',
|
||||
collector: 'Collector',
|
||||
};
|
||||
export const TREE_TYPE_IMAGES: Record<TreeType, string> = {
|
||||
researchable: 'https://i.imgur.com/pJxO2XY.png',
|
||||
premium: 'https://i.imgur.com/mZzSwOU.png',
|
||||
collector: 'https://i.imgur.com/7A0RsG5.png',
|
||||
};
|
||||
export const TREE_TYPE_ICONS: Record<TreeType, Record<TankClass, string>> = {
|
||||
researchable: TANK_ICONS,
|
||||
premium: TANK_ICONS_PREMIUM,
|
||||
collector: TANK_ICONS_COLLECTOR,
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { theme } from '../../../../stitches.config';
|
||||
import { TANK_CLASSES, TREE_TYPES, TREE_TYPE_ICONS } from './constants';
|
||||
|
||||
export type TankClass = (typeof TANK_CLASSES)[number];
|
||||
export type TreeType = (typeof TREE_TYPES)[number];
|
||||
|
||||
export interface ItemProps {
|
||||
tankType?: TankClass;
|
||||
name: string;
|
||||
treeType: TreeType;
|
||||
}
|
||||
|
||||
const TREE_TYPE_COLOR = {
|
||||
researchable: '',
|
||||
premium: '_amber',
|
||||
collector: '_blue',
|
||||
} as const;
|
||||
|
||||
export function Item({ tankType, name, treeType }: ItemProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: 32,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
backgroundColor:
|
||||
theme.colors[`componentInteractive${TREE_TYPE_COLOR[treeType]}`],
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{tankType && (
|
||||
<img
|
||||
alt={tankType}
|
||||
src={TREE_TYPE_ICONS[treeType][tankType]}
|
||||
style={{ width: 14, height: 14 }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
fontSize: 16,
|
||||
whiteSpace: 'nowrap',
|
||||
color: theme.colors[`textLowContrast${TREE_TYPE_COLOR[treeType]}`],
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* {image && (
|
||||
// TODO: remove hardcoded dimensions when satori fixes non-width unloadable images
|
||||
<img
|
||||
alt={name}
|
||||
src={image}
|
||||
width={106}
|
||||
height={32}
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface RootProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface RowProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Row({ children }: RowProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
interface TitleProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
export function Title({ children }: TitleProps) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
color: theme.colors.textHighContrast,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './components/Column';
|
||||
export * from './components/Item';
|
||||
export * from './components/Root';
|
||||
export * from './components/Row';
|
||||
export * from './components/Title';
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Tier } from '../core/blitzkit/tankDefinitions';
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
||||
|
||||
interface TierWeightsProps {
|
||||
weights: TierWeightsRecord;
|
||||
}
|
||||
|
||||
export default function TierWeights({ weights }: TierWeightsProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{Object.entries(weights).map(([tier, weight]) => (
|
||||
<div
|
||||
key={tier}
|
||||
style={{
|
||||
backgroundColor: theme.colors.appBackground1_light,
|
||||
flex: weight,
|
||||
color: theme.colors.textHighContrast_light,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 16,
|
||||
overflow: 'hidden',
|
||||
borderRadius: 8,
|
||||
minWidth: 16,
|
||||
}}
|
||||
>
|
||||
{tier}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
interface TitleBarProps {
|
||||
title: string;
|
||||
image?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function TitleBar({ title, image, description }: TitleBarProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
gap: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '8px 0px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
gap: 4,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{image && (
|
||||
<img
|
||||
alt={title}
|
||||
style={{ width: 32, height: 32, objectFit: 'contain' }}
|
||||
src={image}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 900,
|
||||
background: `linear-gradient(180deg, ${theme.colors.textHighContrast} 0%, ${theme.colors.textLowContrast} 100%)`,
|
||||
backgroundClip: 'text',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user