mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
feat: satori and resvg for rendering
This commit is contained in:
1 parent
e48ea4c307
commit
c834f4f966
34 files changed
+670
-501
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,6 @@ import {
|
||||
ButtonBuilder,
|
||||
SlashCommandBuilder,
|
||||
} from 'discord.js';
|
||||
import escapeHTML from 'escape-html';
|
||||
import GenericStats from '../components/GenericStats/index.js';
|
||||
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
||||
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
||||
@@ -19,7 +18,7 @@ import getBlitzStarsAccount from '../core/blitzstars/getBlitzStarsAccount.js';
|
||||
import cmdName from '../core/interaction/cmdName.js';
|
||||
import addUsernameOption from '../core/options/addUsernameOption.js';
|
||||
import { args } from '../core/process/args.js';
|
||||
import screenshotHTML from '../core/ui/screenshotHTML.js';
|
||||
import render from '../core/ui/render.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate.js';
|
||||
import { AccountInfo, AllStats } from '../types/accountInfo.js';
|
||||
import { PlayerClanData } from '../types/playerClanData.js';
|
||||
@@ -118,22 +117,27 @@ export default {
|
||||
}
|
||||
if (!stats) return;
|
||||
|
||||
const screenshot = await screenshotHTML(
|
||||
Wrapper(
|
||||
TitleBar(
|
||||
escapeHTML(blitzStarsAccount.nickname),
|
||||
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : '',
|
||||
clanData[id]?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||
: undefined,
|
||||
`${periodNames[period]} • ${new Date().toDateString()} • ${
|
||||
BLITZ_SERVERS[server]
|
||||
}`,
|
||||
),
|
||||
const image = await render(
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={blitzStarsAccount.nickname}
|
||||
nameDiscriminator={
|
||||
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
|
||||
}
|
||||
image={
|
||||
clanData[id]?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||
: undefined
|
||||
}
|
||||
description={`${
|
||||
periodNames[period]
|
||||
} • ${new Date().toDateString()} • ${BLITZ_SERVERS[server]}`}
|
||||
/>
|
||||
|
||||
stats.all.battles === 0
|
||||
? NoBattlesInPeriod()
|
||||
: GenericStats([
|
||||
{stats.all.battles === 0 && <NoBattlesInPeriod />}
|
||||
{stats.all.battles > 0 && (
|
||||
<GenericStats
|
||||
stats={[
|
||||
[
|
||||
'Winrate',
|
||||
`${(100 * (stats.all.wins / stats.all.battles)).toFixed(2)}%`,
|
||||
@@ -183,10 +187,12 @@ export default {
|
||||
(stats.all.battles - stats.all.survived_battles)
|
||||
).toFixed(2),
|
||||
],
|
||||
]),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
PoweredByBlitzStars(),
|
||||
),
|
||||
<PoweredByBlitzStars />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
@@ -195,7 +201,7 @@ export default {
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
files: [screenshot],
|
||||
files: [image],
|
||||
components: [actionRow],
|
||||
});
|
||||
|
||||
@@ -3,8 +3,7 @@ import {
|
||||
ButtonBuilder,
|
||||
SlashCommandBuilder,
|
||||
} from 'discord.js';
|
||||
import escapeHTML from 'escape-html';
|
||||
import Breakdown from '../components/Breakdown.js';
|
||||
import * as Breakdown from '../components/Breakdown/index.js';
|
||||
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
||||
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
||||
import TitleBar from '../components/TitleBar.js';
|
||||
@@ -21,7 +20,7 @@ import { tankopedia } from '../core/blitzstars/tankopedia.js';
|
||||
import cmdName from '../core/interaction/cmdName.js';
|
||||
import addUsernameOption from '../core/options/addUsernameOption.js';
|
||||
import { args } from '../core/process/args.js';
|
||||
import screenshotHTML from '../core/ui/screenshotHTML.js';
|
||||
import render from '../core/ui/render.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate.js';
|
||||
import { AccountInfo, AllStats } from '../types/accountInfo.js';
|
||||
import { PlayerClanData } from '../types/playerClanData.js';
|
||||
@@ -30,7 +29,7 @@ import resolveTankName from '../utilities/resolveTankName.js';
|
||||
|
||||
export default {
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -72,43 +71,51 @@ export default {
|
||||
([tankId, tankStats]) => {
|
||||
const career = careerStats[tankId as unknown as number];
|
||||
|
||||
return Breakdown.Row(
|
||||
resolveTankName({
|
||||
tank_id: tankId as unknown as number,
|
||||
name: tankopedia.data[tankId as unknown as number].name,
|
||||
}),
|
||||
tankStats.wins / tankStats.battles,
|
||||
career.wins / career.battles,
|
||||
-Infinity,
|
||||
-Infinity,
|
||||
tankStats.damage_dealt / tankStats.battles,
|
||||
career.damage_dealt / career.battles,
|
||||
tankStats.survived_battles / tankStats.battles,
|
||||
career.survived_battles / career.battles,
|
||||
tankStats.battles,
|
||||
career.battles,
|
||||
tankopedia.data[tankId as unknown as number].images.normal,
|
||||
return (
|
||||
<Breakdown.Row
|
||||
key={tankId}
|
||||
name={resolveTankName({
|
||||
tank_id: tankId as unknown as number,
|
||||
name: tankopedia.data[tankId as unknown as number].name,
|
||||
})}
|
||||
winrate={tankStats.wins / tankStats.battles}
|
||||
careerWinrate={career.wins / career.battles}
|
||||
WN8={-Infinity}
|
||||
careerWN8={-Infinity}
|
||||
damage={tankStats.damage_dealt / tankStats.battles}
|
||||
careerDamage={career.damage_dealt / career.battles}
|
||||
survival={tankStats.survived_battles / tankStats.battles}
|
||||
careerSurvival={career.survived_battles / career.battles}
|
||||
battles={tankStats.battles}
|
||||
careerBattles={career.battles}
|
||||
icon={tankopedia.data[tankId as unknown as number].images.normal}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const screenshot = await screenshotHTML(
|
||||
Wrapper(
|
||||
TitleBar(
|
||||
escapeHTML(accountInfo[id].nickname),
|
||||
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : '',
|
||||
clanData[id]?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||
: undefined,
|
||||
`Today's breakdown • ${new Date().toDateString()} • ${
|
||||
const image = await render(
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={accountInfo[id].nickname}
|
||||
nameDiscriminator={
|
||||
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
|
||||
}
|
||||
image={
|
||||
clanData[id]?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||
: undefined
|
||||
}
|
||||
description={`Today's breakdown • ${new Date().toDateString()} • ${
|
||||
BLITZ_SERVERS[server]
|
||||
}`,
|
||||
),
|
||||
}`}
|
||||
/>
|
||||
|
||||
rows.length === 0 ? NoBattlesInPeriod() : Breakdown.Root(...rows),
|
||||
{rows.length === 0 && <NoBattlesInPeriod />}
|
||||
{rows.length > 0 && <Breakdown.Root>{rows}</Breakdown.Root>}
|
||||
|
||||
PoweredByBlitzStars(),
|
||||
),
|
||||
<PoweredByBlitzStars />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
@@ -117,7 +124,7 @@ export default {
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
files: [screenshot],
|
||||
files: [image],
|
||||
components: [actionRow],
|
||||
});
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import Children from './Children.js';
|
||||
|
||||
const Breakdown = {
|
||||
Root(...children: string[]) {
|
||||
return `
|
||||
<style>
|
||||
.breakdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="breakdown">
|
||||
${Children(children)}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
Row(
|
||||
name: string,
|
||||
winrate: number,
|
||||
careerWinrate: number,
|
||||
wn8: number,
|
||||
careerWn8: number,
|
||||
averageDamage: number,
|
||||
careerAverageDamage: number,
|
||||
survival: number,
|
||||
careerSurvival: number,
|
||||
battles: number,
|
||||
careerBattles: number,
|
||||
icon?: string,
|
||||
) {
|
||||
return `
|
||||
<style>
|
||||
.breakdown-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="breakdown-row">
|
||||
${this.RowDiscriminator(name, icon)}
|
||||
${this.RowStat(
|
||||
'Winrate',
|
||||
`${(winrate * 100).toFixed(2)}%`,
|
||||
`${(careerWinrate * 100).toFixed(2)}%`,
|
||||
winrate - careerWinrate,
|
||||
)}
|
||||
${this.RowStat(
|
||||
'Damage',
|
||||
averageDamage.toFixed(2),
|
||||
careerAverageDamage.toFixed(2),
|
||||
averageDamage - careerAverageDamage,
|
||||
)}
|
||||
${this.RowStat(
|
||||
'Survival',
|
||||
`${(survival * 100).toFixed(2)}%`,
|
||||
`${(careerSurvival * 100).toFixed(2)}%`,
|
||||
survival - careerSurvival,
|
||||
)}
|
||||
${this.RowStat('Battles', battles.toFixed(0), careerBattles.toFixed(0))}
|
||||
</div>
|
||||
`;
|
||||
|
||||
/*
|
||||
${this.RowStat(
|
||||
'WN8',
|
||||
wn8.toFixed(0),
|
||||
careerWn8.toFixed(0),
|
||||
wn8 - careerWn8,
|
||||
)}
|
||||
*/
|
||||
},
|
||||
|
||||
RowDiscriminator(name: string, icon?: string) {
|
||||
return `
|
||||
<style>
|
||||
.breakdown-row-discriminator {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
width: 0px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.breakdown-row-discriminator-icon {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.breakdown-row-discriminator-name {
|
||||
color: #A0A0A0;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="breakdown-row-discriminator">
|
||||
${
|
||||
icon
|
||||
? `<img src="${icon}" class="breakdown-row-discriminator-icon" />`
|
||||
: ''
|
||||
}
|
||||
<span class="breakdown-row-discriminator-name">${name}</span>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
RowStat(name: string, value: string, career: string, delta?: number) {
|
||||
return `
|
||||
<style>
|
||||
.breakdown-row-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-career {
|
||||
color: #A0A0A0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-name {
|
||||
color: #A0A0A0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-value {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-value-delta {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-value-text {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="breakdown-row-stat">
|
||||
<span class="breakdown-row-stat-career">${career}</span>
|
||||
|
||||
<div class="breakdown-row-stat-value">
|
||||
${
|
||||
delta
|
||||
? `<img src="${
|
||||
(delta ?? 0) > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}" class="breakdown-row-stat-value-delta" />`
|
||||
: ''
|
||||
}
|
||||
|
||||
<span class="breakdown-row-stat-value-text">${value}</span>
|
||||
</div>
|
||||
|
||||
<span class="breakdown-row-stat-name">${name}</span>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
export default Breakdown;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface RootProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { RowDiscriminator } from './RowDiscriminator.js';
|
||||
import { RowStat } from './RowStat.js';
|
||||
|
||||
export interface RowProps {
|
||||
name: string;
|
||||
winrate: number;
|
||||
careerWinrate: number;
|
||||
WN8: number;
|
||||
careerWN8: number;
|
||||
damage: number;
|
||||
careerDamage: number;
|
||||
survival: number;
|
||||
careerSurvival: number;
|
||||
battles: number;
|
||||
careerBattles: number;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export function Row({
|
||||
name,
|
||||
winrate,
|
||||
careerWinrate,
|
||||
WN8,
|
||||
careerWN8,
|
||||
damage: averageDamage,
|
||||
careerDamage: careerAverageDamage,
|
||||
survival,
|
||||
careerSurvival,
|
||||
battles,
|
||||
careerBattles,
|
||||
icon,
|
||||
}: RowProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<RowDiscriminator name={name} icon={icon} />
|
||||
<RowStat
|
||||
name="Winrate"
|
||||
value={`${(winrate * 100).toFixed(2)}%`}
|
||||
career={`${(careerWinrate * 100).toFixed(2)}%`}
|
||||
delta={winrate - careerWinrate}
|
||||
/>
|
||||
<RowStat
|
||||
name="Damage"
|
||||
value={averageDamage.toFixed(2)}
|
||||
career={careerAverageDamage.toFixed(2)}
|
||||
delta={averageDamage - careerAverageDamage}
|
||||
/>
|
||||
<RowStat
|
||||
name="Survival"
|
||||
value={`${(survival * 100).toFixed(2)}%`}
|
||||
career={`${(careerSurvival * 100).toFixed(2)}%`}
|
||||
delta={survival - careerSurvival}
|
||||
/>
|
||||
<RowStat
|
||||
name="Battles"
|
||||
value={battles.toFixed(0)}
|
||||
career={careerBattles.toFixed(0)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
export interface RowDiscriminatorProps {
|
||||
name: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export function RowDiscriminator({ name, icon }: RowDiscriminatorProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
width: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{icon && (
|
||||
<img
|
||||
src={icon}
|
||||
style={{ width: '100%', flex: 1, objectFit: 'cover' }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
color: '#A0A0A0',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export interface RowStatProps {
|
||||
name: string;
|
||||
value: string;
|
||||
career: string;
|
||||
delta?: number;
|
||||
}
|
||||
|
||||
export function RowStat({ name, value, career, delta }: RowStatProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#A0A0A0', fontSize: 12 }}>{career}</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{delta && (
|
||||
<img
|
||||
src={
|
||||
(delta ?? 0) > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
style={{ width: 12, height: 12 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span style={{ color: 'white', fontSize: 16, fontWeight: 'bold' }}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span style={{ color: '#A0A0A0', fontSize: 12 }}>{name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './components/Root.js';
|
||||
export * from './components/Row.js';
|
||||
export * from './components/RowDiscriminator.js';
|
||||
export * from './components/RowStat.js';
|
||||
@@ -1,5 +0,0 @@
|
||||
export type ChildrenArgument = string | string[];
|
||||
|
||||
export default function Children(children: string[]) {
|
||||
return children.join('\n');
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Stat } from '../index.js';
|
||||
import GenericStatsRow from './GenericStatsRow.js';
|
||||
|
||||
export default function GenericStatsColumn(stats: Stat[]) {
|
||||
return `
|
||||
<style>
|
||||
.generic-stats-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="generic-stats-column">
|
||||
${stats.map((stat) => GenericStatsRow(stat)).join('\n')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Stat } from '../index.js';
|
||||
import GenericStatsRow from './GenericStatsRow.js';
|
||||
|
||||
export interface GenericStatsColumnProps {
|
||||
stats: Stat[];
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Stat } from '../index.js';
|
||||
|
||||
export default function GenericStatsRow(stat: Stat) {
|
||||
return `
|
||||
<style>
|
||||
.generic-stats-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.generic-stats-label {
|
||||
color: #A0A0A0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.generic-stats-value {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="generic-stats-row">
|
||||
<span class="generic-stats-label">${stat[0]}</span>
|
||||
<span class="generic-stats-value">${stat[1]}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Stat } from '../index.js';
|
||||
|
||||
export interface GenericStatsRowProps {
|
||||
stat: Stat;
|
||||
}
|
||||
|
||||
export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#A0A0A0', fontSize: 16 }}>{stat[0]}</span>
|
||||
<span style={{ color: 'white', fontWeight: 'bold', fontSize: 16 }}>
|
||||
{stat[1]}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,15 @@ import GenericStatsColumn from './components/GenericStatsColumn.js';
|
||||
|
||||
export type Stat = [string, string | number];
|
||||
|
||||
export default function GenericStats(stats: Stat[], columnCount = 2) {
|
||||
export interface GenericStatsProps {
|
||||
stats: Stat[];
|
||||
columnCount?: number;
|
||||
}
|
||||
|
||||
export default function GenericStats({
|
||||
stats,
|
||||
columnCount = 2,
|
||||
}: GenericStatsProps) {
|
||||
const itemsPerRow = Math.ceil(stats.length / columnCount);
|
||||
const columns: Stat[][] = [];
|
||||
|
||||
@@ -12,16 +20,14 @@ export default function GenericStats(stats: Stat[], columnCount = 2) {
|
||||
columns[column].push(stat);
|
||||
});
|
||||
|
||||
return `
|
||||
<style>
|
||||
.generic-stats {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="generic-stats">
|
||||
${columns.map((column) => GenericStatsColumn(column)).join('\n')}
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 32 }}>
|
||||
{columns.map((stats) => (
|
||||
<GenericStatsColumn
|
||||
key={stats.map((stat) => stat[0]).join('-')}
|
||||
stats={stats}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
`;
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
export default function NoBattlesInPeriod() {
|
||||
return `
|
||||
<style>
|
||||
.no-battles-in-period {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.no-battles-in-period-text {
|
||||
font-size: 16px;
|
||||
color: #A0A0A0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="no-battles-in-period">
|
||||
<span class="no-battles-in-period-text">No battles played in this period</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export default function NoBattlesInPeriod() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: '#A0A0A0' }}>
|
||||
No battles played in this period
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
export default function PoweredByBlitzStars() {
|
||||
return `
|
||||
<style>
|
||||
.powered-by-blitz-stars {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.powered-by-blitz-stars-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.powered-by-blitz-stars-text {
|
||||
font-size: 16px;
|
||||
color: #A0A0A0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="powered-by-blitz-stars">
|
||||
<img class="powered-by-blitz-stars-icon" src="https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png" />
|
||||
<span class="powered-by-blitz-stars-text">Powered by BlitzStars</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export default function PoweredByBlitzStars() {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
|
||||
<img
|
||||
src="https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png"
|
||||
style={{ height: 16, width: 16 }}
|
||||
/>
|
||||
<span style={{ color: '#A0A0A0', fontSize: 16 }}>
|
||||
Powered by BlitzStars
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import Children from './Children.js';
|
||||
|
||||
export default function Root(...children: string[]) {
|
||||
return `
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:[email protected]');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:[email protected]');
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: 'Roboto Flex', sans-serif;
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="root">
|
||||
${Children(children)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
export default function TitleBar(
|
||||
name: string,
|
||||
nameDiscriminator: string,
|
||||
image?: string,
|
||||
description?: string,
|
||||
) {
|
||||
return `
|
||||
<style>
|
||||
.title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-bar-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-bar-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.title-bar-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title-bar-name {
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.title-bar-discriminator {
|
||||
color: #A0A0A0;
|
||||
}
|
||||
|
||||
.title-bar-description {
|
||||
color: #A0A0A0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.title-bar-blitzkrieg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-info">
|
||||
${image ? `<img class="title-bar-avatar" src="${image}" />` : ''}
|
||||
|
||||
<div class="title-bar-text">
|
||||
<span class="title-bar-name">
|
||||
${name}
|
||||
<span class="title-bar-discriminator">${nameDiscriminator}</span>
|
||||
</span>
|
||||
|
||||
<span class="title-bar-description">${description}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img class="title-bar-blitzkrieg" src="https://i.imgur.com/35phKIu.png" />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
export interface TitleBarProps {
|
||||
name: string;
|
||||
nameDiscriminator?: string;
|
||||
image?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function TitleBar({
|
||||
name,
|
||||
nameDiscriminator,
|
||||
image,
|
||||
description,
|
||||
}: TitleBarProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{image && <img style={{ width: 64, height: 64 }} src={image} />}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: 'white',
|
||||
fontWeight: 900,
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
{nameDiscriminator && (
|
||||
<span style={{ color: '#A0A0A0' }}>{nameDiscriminator}</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span style={{ color: '#A0A0A0', fontSize: 16 }}>{description}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
style={{ width: 64, height: 64 }}
|
||||
src="https://i.imgur.com/35phKIu.png"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import Children from './Children.js';
|
||||
|
||||
export default function Wrapper(...children: string[]) {
|
||||
return `
|
||||
<style>
|
||||
#wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 32px;
|
||||
width: 640px;
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="wrapper">
|
||||
${Children(children)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface WrapperProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Wrapper({ children }: WrapperProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: 16,
|
||||
gap: 32,
|
||||
width: 640,
|
||||
background: 'black',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Resvg } from '@resvg/resvg-js';
|
||||
import { readFileSync } from 'fs';
|
||||
import { ReactNode } from 'react';
|
||||
import satori from 'satori';
|
||||
|
||||
const robotoFlex = readFileSync('src/assets/fonts/Roboto.ttf');
|
||||
const robotoFlexBold = readFileSync('src/assets/fonts/Roboto-Bold.ttf');
|
||||
const robotoFlexBlack = readFileSync('src/assets/fonts/Roboto-Black.ttf');
|
||||
|
||||
export default async function render(element: ReactNode) {
|
||||
const svg = await satori(element, {
|
||||
width: 640,
|
||||
fonts: [
|
||||
{ data: robotoFlex, name: 'Roboto Flex', weight: 400 },
|
||||
{ data: robotoFlexBold, name: 'Roboto Flex', weight: 700 },
|
||||
{ data: robotoFlexBlack, name: 'Roboto Flex', weight: 900 },
|
||||
],
|
||||
});
|
||||
const png = new Resvg(svg).render().asPng();
|
||||
|
||||
return png;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import Root from '../../components/Root.js';
|
||||
import { browser } from '../../index.js';
|
||||
|
||||
export default async function screenshotHTML(children: string) {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewportSize({ width: 640, height: 1 });
|
||||
await page.setContent(Root(children));
|
||||
const screenshot = await page
|
||||
.locator('#root')
|
||||
.screenshot({ omitBackground: true, scale: 'css' });
|
||||
await page.close();
|
||||
|
||||
return screenshot;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { chromium } from '@playwright/test';
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { args } from './core/process/args.js';
|
||||
import { registerErrorHandlers } from './events/error.js';
|
||||
@@ -17,5 +16,3 @@ client
|
||||
.on('guildMemberAdd', guildMemberAdd)
|
||||
.on('interactionCreate', interactionCreate)
|
||||
.login(args['discord-token']);
|
||||
|
||||
export const browser = await chromium.launch();
|
||||
File renamed without changes.
Reference in new issue
Block a user