feat: migration 3

This commit is contained in:
TrèsAbhi committed 2024-09-06 23:26:35 -05:00
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'
}
/>
);
}
+19
View File
@@ -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>
);
}
+54
View File
@@ -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>
);
}