Skip to main content

Profile UI plugins

The player drawer is the panel that slides in when you click a player name anywhere in JNbot — in chat, the player list, or a recording. What that drawer shows is fully under your control: a profile-UI plugin decides which cards appear, what's in them, and where the data comes from.

A profile-UI plugin is one JavaScript or TypeScript file that exports a main(args) function. Every time a drawer opens, JNbot calls main, hands it some read-only data about the player, and renders whatever you return.

You attach a profile-UI plugin to a server in step 6 of the profile wizard. If a server has no profile-UI plugin, JNbot falls back to a built-in minimal layout.

Live preview

While editing, the wizard and the code editor both show a live preview of the drawer for a real alt (pick which one from the dropdown). You don't have to join a server to see your changes.


The main function​

async function main(args) {
const p = new ProfileBuilder().player({ username: args.username });

p.identity('left')
.badge('Verified', 'emerald')
.count('level', 42);

return p.build();
}
  • main may be async — await your fetch calls freely.
  • Return the result of new ProfileBuilder()....build() (or a plain object of the same shape).
  • The page is validated before it renders: unknown fields, malformed values, and over-long lists are dropped silently, so a small mistake never breaks the drawer.

What you get: args​

main receives a single read-only args object. Everything in it is a snapshot — you can read it, but changes don't affect JNbot.

FieldDescription
serverThe active server id (e.g. "jartexnetwork").
usernameThe player being looked up. May not be one of your alts.
altThe matching alt (username, premium, UID, email) when username is yours, else null.
serverAltInfoPer-server data for the alt: configured ranks and gkits by gamemode. Passwords are never exposed. null if not yours.
statsLocal stats for the alt: joinCount, lastJoined, playTime, createdOn. null if not yours.
profileThe server's profile metadata + per-gamemode rank/gkit catalog (prices & colors).
fetchA restricted fetch you can use to pull data from an external API (see below).
note

alt, serverAltInfo, and stats are only populated when the player is one of your alts. For anyone else (e.g. a stranger in chat) they're null — always guard against that before reading them.

Fetching external data​

args.fetch lets you call a public stats API. It's deliberately limited:

  • Only http/https URLs.
  • Response body capped at 1 MiB.
  • 6-second timeout per request.
const res = await args.fetch(
'https://stats.example.net/api/profile/' + encodeURIComponent(args.username),
);
// res = { status, ok, text, json }
const data = res.ok ? res.json : {};

It resolves to { status, ok, text, json }. json is auto-parsed when the response is application/json, otherwise null — use text for anything else.

tip

Fetch each gamemode/endpoint in parallel with Promise.all and add a .catch(() => null) per request, so one slow or failing endpoint doesn't stall the whole drawer. The whole action shares a single time budget (see Limits).


Building the page​

Start with a ProfileBuilder, set the header with .player(...), add one or more sections, and finish with .build().

const p = new ProfileBuilder().player({
username: args.username,
displayRank: 'Warrior',
skinUrl: 'https://crafthead.net/armor/body/' + encodeURIComponent(args.username),
});

The player header​

.player(data) sets the big header. Fields:

FieldDescription
usernameRequired. The in-game name.
skinUrlOverride the body skin image. Defaults to a crafthead.net render.
displayNameOverride the big name. Defaults to username.
displayRankA highlighted rank pill shown before the name.

Sections​

Each section builder is created from the ProfileBuilder and returns itself, so calls chain. Every section takes an optional column — 'left' or 'right' (default 'left') — that controls which side of the two-column drawer it lands in.

BuilderRenders
.identity(column)Badges and headline count tiles under the player.
.kpis(column)A strip of big KPI cards (a label + value each).
.stats(column)A multi-gamemode stats card with a gamemode selector.
.ranks(column)A grid of rank pills.
.gkits(column)Gkit tags grouped by gamemode.
.userList(title, column)A list of players (friends/clan) — each row opens that player's drawer.

Identity​

p.identity('left')
.badge('Email verified', 'emerald') // (label, tone?, icon?)
.badge('Discord linked', 'sky', 'discord')
.count('level', 42) // (label, value)
.count('friends', 18);

KPI strip​

p.kpis('right')
.item('account worth', '€ 295.00', { icon: 'money', tone: 'amber' })
.item('play time', '12h 30m', { icon: 'clock', tone: 'cyan' });

item(label, value, opts?) — opts.icon is one of money, hash, clock, star, trophy, bolt; opts.tone is a tone.

Stats​

Add a gamemode tile, then attach its stat rows. Rows only appear when that tile is the selected one.

const s = p.stats('right');
s.gamemode('kitpvp', 'KitPvP', { icon: '🗡️', accent: '#f0a23b' })
.add('Kills', '12,847', { rank: '#142', delta: '+318' })
.add('Deaths', '3,201');
s.gamemode('bedwars', 'BedWars', { icon: '🛏️' })
.add('Wins', '540');
s.selected('kitpvp'); // which tile is active by default

.gamemode(id, label, opts?) returns a sub-builder whose .add(label, value, opts?) writes rows for that gamemode (opts may include a rank and a delta). opts on the gamemode itself takes an emoji icon and a hex accent.

Ranks​

p.ranks('right')
.add('Warrior', 'KitPvP', 'amber', { emphasis: 'high' })
.add('Member', 'Prison', 'slate', { emphasis: 'low' });

.add(name, server?, tone?, opts?) — opts.emphasis is 'high' (filled) or 'low' (muted/outlined), handy for contrasting two classes of rank.

Gkits​

p.gkits('left')
.group('KitPvP', ['Warrior', 'Knight'])
.group('Factions', ['Immortal']);

User list​

const fl = p.userList('Friends · 18', 'left');
friends.forEach((f) => fl.add(f.username, { online: true, extra: 'lvl 12' }));

.add(username, opts?) — opts.online toggles the online dot, opts.extra is secondary text on the right.

Finishing​

return p.build();

Tones​

Several builders accept a tone for color. The available tones are:

amber · emerald · rose · indigo · sky · purple · slate · red · cyan


Full example​

async function main(args) {
const { username, stats, fetch } = args;

// Pull a public profile API (guard the failure).
const res = await fetch(
'https://stats.example.net/api/profile/' + encodeURIComponent(username),
).catch(() => null);
const data = res && res.json ? res.json : {};

const p = new ProfileBuilder().player({
username,
displayRank: data.rank,
skinUrl: 'https://crafthead.net/armor/body/' + encodeURIComponent(username),
});

// Identity badges + counts.
const id = p.identity('left');
if (data.email_verified) id.badge('Email verified', 'emerald');
id.count('level', data.level ?? '—').count('friends', (data.friends || []).length);

// KPIs — only show local stats when this is one of your alts.
const kpi = p.kpis('right').item('network level', 'Lvl ' + (data.level ?? '—'), {
icon: 'star',
tone: 'amber',
});
if (stats) {
kpi.item('times joined', String(stats.joinCount ?? 0), { icon: 'emerald', tone: 'emerald' });
}

// Stats card.
const s = p.stats('right');
s.gamemode('kitpvp', 'KitPvP', { icon: '🗡️' }).add('Kills', '12,847');
s.selected('kitpvp');

// Friends list (each row opens that player's drawer).
(data.friends || []).forEach((f) => p.userList('Friends', 'left').add(f.username));

return p.build();
}

Limits​

Your page is sanitized before it renders. The caps you're most likely to hit:

WhatCap
Action run time5 seconds
Sections per page24
KPI items8
Ranks24
Stat gamemodes / rows each16 / 12
User-list rows80
Gkit groups / items each16 / 24
Badges8

Text is truncated to a safe length and anything that can't be coerced to the expected type is dropped — so an over-long label or a stray field won't error, it just won't show.

Sandbox

Profile-UI plugins run in a locked-down sandbox: no file system, no Node modules, and the only way to reach the network is args.fetch. If your code throws or times out, the drawer shows an error in the preview's log panel — keep the console open while developing.