fuelex-leaderboard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # Fuelex Leaderboard
2
+
3
+ <div align="center">
4
+
5
+ ![Fuelex Banner](https://fuelex-bot.dev/banner.png)
6
+
7
+ **Premium Discord Leaderboard UI Package**
8
+
9
+ [![npm version](https://img.shields.io/npm/v/fuelex-leaderboard.svg?style=flat-square&color=FFD700)](https://www.npmjs.com/package/fuelex-leaderboard)
10
+ [![License](https://img.shields.io/badge/license-MIT-yellow.svg?style=flat-square)](LICENSE)
11
+ [![Discord](https://img.shields.io/badge/Discord-Fuelex-FFD700?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/fuelex)
12
+
13
+ *High-quality, customizable leaderboard image generator for Discord bots*
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## ✨ Features
20
+
21
+ - 🎨 **Premium Black & Yellow Theme** - Sleek, modern design with neon glow effects
22
+ - 🏆 **Tier-based Rank Styling** - Gold, Silver, Bronze gradients for top 3
23
+ - 📊 **Multiple Stat Types** - Voice, XP, Messages, Invites, Economy, Levels
24
+ - ⚡ **High Performance** - Built with `@napi-rs/canvas` for native speed
25
+ - 🔧 **Fully Customizable** - Easy to configure and extend
26
+ - 📱 **Discord Optimized** - Perfect dimensions for embeds and messages
27
+
28
+ ---
29
+
30
+ ## 📦 Installation
31
+
32
+ ```bash
33
+ npm install fuelex-leaderboard
34
+ ```
35
+
36
+ ---
37
+
38
+ ## 🚀 Quick Start
39
+
40
+ ```javascript
41
+ const { LeaderboardCard } = require('fuelex-leaderboard');
42
+ const fs = require('fs');
43
+
44
+ // Create a voice leaderboard
45
+ const leaderboard = new LeaderboardCard({
46
+ type: 'voice',
47
+ title: 'Voice Leaderboard',
48
+ users: [
49
+ { rank: 1, userId: '123', username: 'DragonSlayer', value: 43200, isActive: true },
50
+ { rank: 2, userId: '456', username: 'NightWolf', value: 38500, isActive: true },
51
+ { rank: 3, userId: '789', username: 'PhoenixRider', value: 32100, isActive: false },
52
+ // ... more users
53
+ ],
54
+ rowCount: 10,
55
+ footer: 'fuelex-bot.dev'
56
+ });
57
+
58
+ // Generate the image
59
+ const buffer = await leaderboard.render();
60
+ fs.writeFileSync('leaderboard.png', buffer);
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 📋 Options
66
+
67
+ | Option | Type | Default | Description |
68
+ |--------|------|---------|-------------|
69
+ | `type` | `string` | `'xp'` | Leaderboard type: `voice`, `xp`, `messages`, `invites`, `economy`, `levels` |
70
+ | `title` | `string` | Auto-generated | Custom title for the leaderboard |
71
+ | `users` | `Array` | `[]` | Array of user objects |
72
+ | `rowCount` | `number` | `10` | Number of rows to display (5, 10, 15, 20) |
73
+ | `footer` | `string` | `null` | Optional footer text |
74
+ | `showHeader` | `boolean` | `true` | Whether to show the header section |
75
+ | `showAvatars` | `boolean` | `true` | Whether to display user avatars |
76
+
77
+ ---
78
+
79
+ ## 👤 User Object
80
+
81
+ ```javascript
82
+ {
83
+ rank: 1, // User's rank position (1-indexed)
84
+ userId: '123456789', // Unique identifier
85
+ username: 'Player1', // Display name
86
+ value: 12345, // Stat value (formatted based on type)
87
+ avatar: 'https://...', // Optional avatar URL
88
+ isActive: true // Optional status indicator
89
+ }
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 🎨 Stat Types
95
+
96
+ | Type | Icon | Value Format |
97
+ |------|------|--------------|
98
+ | `voice` | 🔊 Speaker | Duration (12h 34m) |
99
+ | `xp` | ⭐ Star | Number with XP suffix |
100
+ | `messages` | 💬 Message | Plain number |
101
+ | `invites` | 👥 User | Plain number |
102
+ | `economy` | 💰 Coin | Currency format ($1,234) |
103
+ | `levels` | 🏆 Trophy | Level prefix (Lv. 50) |
104
+
105
+ ---
106
+
107
+ ## 🖼️ Output Formats
108
+
109
+ ```javascript
110
+ // PNG (default, best for Discord)
111
+ const pngBuffer = await leaderboard.render();
112
+
113
+ // JPEG (smaller file size)
114
+ const jpegBuffer = await leaderboard.renderJPEG(90); // quality: 0-100
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 🎯 Discord.js Integration
120
+
121
+ ```javascript
122
+ const { AttachmentBuilder } = require('discord.js');
123
+ const { LeaderboardCard } = require('fuelex-leaderboard');
124
+
125
+ async function sendLeaderboard(interaction) {
126
+ const leaderboard = new LeaderboardCard({
127
+ type: 'voice',
128
+ users: await fetchTopUsers(),
129
+ rowCount: 10
130
+ });
131
+
132
+ const buffer = await leaderboard.render();
133
+ const attachment = new AttachmentBuilder(buffer, { name: 'leaderboard.png' });
134
+
135
+ await interaction.reply({ files: [attachment] });
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 🎨 Color Palette
142
+
143
+ | Element | Color | Hex |
144
+ |---------|-------|-----|
145
+ | Background | Deep Matte Black | `#0A0A0A` |
146
+ | Card | Charcoal | `#1A1A1A` |
147
+ | Neon Yellow | Primary Accent | `#FFEA00` |
148
+ | Gold (#1) | Gradient | `#FFD700 → #FFA500` |
149
+ | Silver (#2) | Muted | `#C0C0C0` |
150
+ | Bronze (#3) | Warm | `#CD7F32` |
151
+
152
+ ---
153
+
154
+ ## 📁 Project Structure
155
+
156
+ ```
157
+ fuelex-leaderboard/
158
+ ├── src/
159
+ │ ├── index.js # Main entry point
160
+ │ ├── core/
161
+ │ │ └── theme.js # Colors, constants, utilities
162
+ │ ├── components/
163
+ │ │ ├── LeaderboardCard.js
164
+ │ │ ├── RankBadge.js
165
+ │ │ ├── StatIcon.js
166
+ │ │ └── UserRow.js
167
+ │ └── utils/
168
+ │ ├── effects.js # Glow, shadows, gradients
169
+ │ └── fonts.js # Font management
170
+ ├── examples/
171
+ │ └── basic.js # Usage examples
172
+ ├── output/ # Generated images
173
+ ├── package.json
174
+ └── README.md
175
+ ```
176
+
177
+ ---
178
+
179
+ ## 🔗 Links
180
+
181
+ - **Website**: [fuelex-bot.dev](https://fuelex-bot.dev)
182
+ - **Discord**: [Join Server](https://discord.gg/fuelex)
183
+ - **GitHub**: [fuelex/fuelex-leaderboard](https://github.com/fuelex/fuelex-leaderboard)
184
+
185
+ ---
186
+
187
+ <div align="center">
188
+
189
+ **Built with 💛 by the Fuelex Team**
190
+
191
+ </div>
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Fuelex Leaderboard - Example Usage
3
+ * Demonstrates generating leaderboard images
4
+ */
5
+
6
+ const { LeaderboardCard } = require('../src/index');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ // Sample user data
11
+ const sampleUsers = [
12
+ { rank: 1, userId: '1', username: 'DragonSlayer', value: 43200, isActive: true },
13
+ { rank: 2, userId: '2', username: 'NightWolf', value: 38500, isActive: true },
14
+ { rank: 3, userId: '3', username: 'PhoenixRider', value: 32100, isActive: false },
15
+ { rank: 4, userId: '4', username: 'ShadowHunter', value: 28900, isActive: true },
16
+ { rank: 5, userId: '5', username: 'CrystalMage', value: 25400, isActive: true },
17
+ { rank: 6, userId: '6', username: 'StormBringer', value: 21800, isActive: false },
18
+ { rank: 7, userId: '7', username: 'FrostKnight', value: 18200, isActive: true },
19
+ { rank: 8, userId: '8', username: 'BlazeMaster', value: 15600, isActive: true },
20
+ { rank: 9, userId: '9', username: 'VoidWalker', value: 12300, isActive: false },
21
+ { rank: 10, userId: '10', username: 'ThunderGod', value: 9800, isActive: true }
22
+ ];
23
+
24
+ async function generateExamples() {
25
+ const outputDir = path.join(__dirname, '../output');
26
+
27
+ // Create output directory if it doesn't exist
28
+ if (!fs.existsSync(outputDir)) {
29
+ fs.mkdirSync(outputDir, { recursive: true });
30
+ }
31
+
32
+ console.log('🎨 Generating Fuelex Leaderboard examples...\n');
33
+
34
+ // Generate Voice Leaderboard
35
+ console.log('📊 Creating Voice Leaderboard...');
36
+ const voiceLeaderboard = new LeaderboardCard({
37
+ type: 'voice',
38
+ title: 'Voice Leaderboard',
39
+ users: sampleUsers,
40
+ rowCount: 10,
41
+ footer: 'fuelex-bot.dev'
42
+ });
43
+
44
+ const voiceBuffer = await voiceLeaderboard.render();
45
+ fs.writeFileSync(path.join(outputDir, 'voice_leaderboard.png'), voiceBuffer);
46
+ console.log(' ✓ Saved: voice_leaderboard.png');
47
+
48
+ // Generate XP Leaderboard
49
+ console.log('📊 Creating XP Leaderboard...');
50
+ const xpLeaderboard = new LeaderboardCard({
51
+ type: 'xp',
52
+ title: 'XP Leaderboard',
53
+ users: sampleUsers.map(u => ({ ...u, value: u.value * 10 })),
54
+ rowCount: 10,
55
+ footer: 'fuelex-bot.dev'
56
+ });
57
+
58
+ const xpBuffer = await xpLeaderboard.render();
59
+ fs.writeFileSync(path.join(outputDir, 'xp_leaderboard.png'), xpBuffer);
60
+ console.log(' ✓ Saved: xp_leaderboard.png');
61
+
62
+ // Generate Economy Leaderboard
63
+ console.log('📊 Creating Economy Leaderboard...');
64
+ const economyLeaderboard = new LeaderboardCard({
65
+ type: 'economy',
66
+ title: 'Richest Members',
67
+ users: sampleUsers.map(u => ({ ...u, value: u.value * 100 })),
68
+ rowCount: 10,
69
+ footer: 'fuelex-bot.dev'
70
+ });
71
+
72
+ const economyBuffer = await economyLeaderboard.render();
73
+ fs.writeFileSync(path.join(outputDir, 'economy_leaderboard.png'), economyBuffer);
74
+ console.log(' ✓ Saved: economy_leaderboard.png');
75
+
76
+ // Generate Messages Leaderboard
77
+ console.log('📊 Creating Messages Leaderboard...');
78
+ const messagesLeaderboard = new LeaderboardCard({
79
+ type: 'messages',
80
+ title: 'Most Active Chatters',
81
+ users: sampleUsers.map(u => ({ ...u, value: Math.floor(u.value / 10) })),
82
+ rowCount: 10,
83
+ footer: 'fuelex-bot.dev'
84
+ });
85
+
86
+ const messagesBuffer = await messagesLeaderboard.render();
87
+ fs.writeFileSync(path.join(outputDir, 'messages_leaderboard.png'), messagesBuffer);
88
+ console.log(' ✓ Saved: messages_leaderboard.png');
89
+
90
+ // Generate Invites Leaderboard
91
+ console.log('📊 Creating Invites Leaderboard...');
92
+ const invitesLeaderboard = new LeaderboardCard({
93
+ type: 'invites',
94
+ title: 'Top Inviters',
95
+ users: sampleUsers.map(u => ({ ...u, value: Math.floor(u.value / 1000) })),
96
+ rowCount: 10,
97
+ footer: 'fuelex-bot.dev'
98
+ });
99
+
100
+ const invitesBuffer = await invitesLeaderboard.render();
101
+ fs.writeFileSync(path.join(outputDir, 'invites_leaderboard.png'), invitesBuffer);
102
+ console.log(' ✓ Saved: invites_leaderboard.png');
103
+
104
+ // Generate Levels Leaderboard
105
+ console.log('📊 Creating Levels Leaderboard...');
106
+ const levelsLeaderboard = new LeaderboardCard({
107
+ type: 'levels',
108
+ title: 'Top Levels',
109
+ users: sampleUsers.map((u, i) => ({ ...u, value: 100 - i * 8 })),
110
+ rowCount: 10,
111
+ footer: 'fuelex-bot.dev'
112
+ });
113
+
114
+ const levelsBuffer = await levelsLeaderboard.render();
115
+ fs.writeFileSync(path.join(outputDir, 'levels_leaderboard.png'), levelsBuffer);
116
+ console.log(' ✓ Saved: levels_leaderboard.png');
117
+
118
+ // Generate Compact (5 rows)
119
+ console.log('📊 Creating Compact Leaderboard (5 rows)...');
120
+ const compactLeaderboard = new LeaderboardCard({
121
+ type: 'xp',
122
+ title: 'Top 5 Players',
123
+ users: sampleUsers.slice(0, 5),
124
+ rowCount: 5,
125
+ footer: 'fuelex-bot.dev'
126
+ });
127
+
128
+ const compactBuffer = await compactLeaderboard.render();
129
+ fs.writeFileSync(path.join(outputDir, 'compact_leaderboard.png'), compactBuffer);
130
+ console.log(' ✓ Saved: compact_leaderboard.png');
131
+
132
+ console.log('\n✨ All examples generated successfully!');
133
+ console.log(`📁 Output directory: ${outputDir}`);
134
+ }
135
+
136
+ // Run examples
137
+ generateExamples().catch(console.error);
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "fuelex-leaderboard",
3
+ "version": "1.0.0",
4
+ "description": "Premium Discord leaderboard UI generator for Fuelex Bot - Black & Yellow theme",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "node test/generate.js",
8
+ "example": "node examples/basic.js"
9
+ },
10
+ "keywords": [
11
+ "discord",
12
+ "leaderboard",
13
+ "fuelex",
14
+ "canvas",
15
+ "image",
16
+ "generator",
17
+ "bot",
18
+ "voice",
19
+ "xp",
20
+ "levels",
21
+ "economy"
22
+ ],
23
+ "author": "Fuelex Team",
24
+ "license": "MIT",
25
+ "homepage": "https://fuelex-bot.dev",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/fuelex/fuelex-leaderboard"
29
+ },
30
+ "dependencies": {
31
+ "@napi-rs/canvas": "^0.1.65"
32
+ },
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ }
36
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Fuelex Leaderboard - Main Leaderboard Card Component
3
+ * Premium Discord leaderboard image generator
4
+ */
5
+
6
+ const { createCanvas, loadImage } = require('@napi-rs/canvas');
7
+ const { COLORS, SIZES, getStatConfig } = require('../core/theme');
8
+ const { drawRoundedRect, applyShadow, clearShadow } = require('../utils/effects');
9
+ const { registerFonts, getFont } = require('../utils/fonts');
10
+ const { drawUserRow } = require('./UserRow');
11
+
12
+ /**
13
+ * LeaderboardCard - Main class for generating leaderboard images
14
+ */
15
+ class LeaderboardCard {
16
+ /**
17
+ * Create a new LeaderboardCard
18
+ * @param {object} options - Leaderboard options
19
+ * @param {string} options.type - Leaderboard type (voice, xp, messages, invites, economy, levels)
20
+ * @param {string} [options.title] - Custom title (auto-generated if not provided)
21
+ * @param {Array} options.users - Array of user data
22
+ * @param {number} [options.rowCount=10] - Number of rows to display
23
+ * @param {string} [options.footer] - Footer text
24
+ * @param {boolean} [options.showHeader=true] - Whether to show the header
25
+ * @param {boolean} [options.showAvatars=true] - Whether to show user avatars
26
+ */
27
+ constructor(options = {}) {
28
+ this.type = options.type || 'xp';
29
+ this.title = options.title || this._generateTitle();
30
+ this.users = options.users || [];
31
+ this.rowCount = options.rowCount || 10;
32
+ this.footer = options.footer || null;
33
+ this.showHeader = options.showHeader !== false;
34
+ this.showAvatars = options.showAvatars !== false;
35
+
36
+ // Calculate dimensions
37
+ this.width = SIZES.cardWidth;
38
+ this.headerHeight = this.showHeader ? 60 : 0;
39
+ this.footerHeight = this.footer ? 40 : 0;
40
+ this.rowHeight = SIZES.rowHeight;
41
+ this.rowGap = SIZES.rowGap;
42
+ this.padding = SIZES.cardPadding;
43
+
44
+ // Register fonts on instantiation
45
+ registerFonts();
46
+ }
47
+
48
+ /**
49
+ * Generate default title based on type
50
+ * @returns {string} Generated title
51
+ * @private
52
+ */
53
+ _generateTitle() {
54
+ const statConfig = getStatConfig(this.type);
55
+ return `${statConfig.label} Leaderboard`;
56
+ }
57
+
58
+ /**
59
+ * Calculate total canvas height
60
+ * @returns {number} Canvas height
61
+ * @private
62
+ */
63
+ _calculateHeight() {
64
+ const displayCount = Math.min(this.users.length, this.rowCount);
65
+ const rowsHeight = displayCount * this.rowHeight + (displayCount - 1) * this.rowGap;
66
+ return this.padding * 2 + this.headerHeight + rowsHeight + this.footerHeight;
67
+ }
68
+
69
+ /**
70
+ * Draw the header section
71
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
72
+ * @private
73
+ */
74
+ _drawHeader(ctx) {
75
+ if (!this.showHeader) return;
76
+
77
+ const statConfig = getStatConfig(this.type);
78
+
79
+ // Draw title
80
+ ctx.font = getFont(SIZES.headerFontSize, 'bold');
81
+ ctx.textAlign = 'left';
82
+ ctx.textBaseline = 'middle';
83
+
84
+ // Title with glow
85
+ ctx.save();
86
+ ctx.shadowColor = COLORS.glowYellow;
87
+ ctx.shadowBlur = 10;
88
+ ctx.fillStyle = COLORS.goldenYellow;
89
+ ctx.fillText(this.title, this.padding, this.padding + 30);
90
+ ctx.restore();
91
+
92
+ // Draw type indicator icon
93
+ const { drawIcon } = require('./StatIcon');
94
+ drawIcon(
95
+ ctx,
96
+ statConfig.icon,
97
+ this.width - this.padding - 30,
98
+ this.padding + 18,
99
+ 24,
100
+ COLORS.neonYellow,
101
+ true
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Draw the footer section
107
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
108
+ * @param {number} y - Y position for footer
109
+ * @private
110
+ */
111
+ _drawFooter(ctx, y) {
112
+ if (!this.footer) return;
113
+
114
+ ctx.font = getFont(14, 'regular');
115
+ ctx.textAlign = 'center';
116
+ ctx.textBaseline = 'middle';
117
+ ctx.fillStyle = COLORS.textMuted;
118
+ ctx.fillText(this.footer, this.width / 2, y + 20);
119
+ }
120
+
121
+ /**
122
+ * Draw the main background
123
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
124
+ * @param {number} height - Canvas height
125
+ * @private
126
+ */
127
+ _drawBackground(ctx, height) {
128
+ // Main background
129
+ ctx.fillStyle = COLORS.primaryBg;
130
+ ctx.fillRect(0, 0, this.width, height);
131
+
132
+ // Subtle gradient overlay
133
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
134
+ gradient.addColorStop(0, 'rgba(255, 215, 0, 0.03)');
135
+ gradient.addColorStop(0.5, 'transparent');
136
+ gradient.addColorStop(1, 'rgba(255, 215, 0, 0.02)');
137
+ ctx.fillStyle = gradient;
138
+ ctx.fillRect(0, 0, this.width, height);
139
+
140
+ // Outer border glow
141
+ ctx.save();
142
+ ctx.strokeStyle = COLORS.goldenYellow;
143
+ ctx.lineWidth = 2;
144
+ ctx.shadowColor = COLORS.glowGold;
145
+ ctx.shadowBlur = 15;
146
+ ctx.beginPath();
147
+ ctx.roundRect(1, 1, this.width - 2, height - 2, 16);
148
+ ctx.stroke();
149
+ ctx.restore();
150
+ }
151
+
152
+ /**
153
+ * Load avatar images for users
154
+ * @returns {Promise<Map>} Map of userId to loaded image
155
+ * @private
156
+ */
157
+ async _loadAvatars() {
158
+ const avatarMap = new Map();
159
+
160
+ if (!this.showAvatars) return avatarMap;
161
+
162
+ const loadPromises = this.users.slice(0, this.rowCount).map(async (user) => {
163
+ if (user.avatar) {
164
+ try {
165
+ const img = await loadImage(user.avatar);
166
+ avatarMap.set(user.userId, img);
167
+ } catch (e) {
168
+ // Failed to load avatar, will render without it
169
+ }
170
+ }
171
+ });
172
+
173
+ await Promise.all(loadPromises);
174
+ return avatarMap;
175
+ }
176
+
177
+ /**
178
+ * Render the leaderboard to a canvas buffer
179
+ * @returns {Promise<Buffer>} PNG image buffer
180
+ */
181
+ async render() {
182
+ const height = this._calculateHeight();
183
+ const canvas = createCanvas(this.width, height);
184
+ const ctx = canvas.getContext('2d');
185
+
186
+ // Draw background
187
+ this._drawBackground(ctx, height);
188
+
189
+ // Draw header
190
+ this._drawHeader(ctx);
191
+
192
+ // Load avatars
193
+ const avatarMap = await this._loadAvatars();
194
+
195
+ // Draw user rows
196
+ const startY = this.padding + this.headerHeight;
197
+ const rowWidth = this.width - this.padding * 2;
198
+ const displayUsers = this.users.slice(0, this.rowCount);
199
+
200
+ for (let i = 0; i < displayUsers.length; i++) {
201
+ const user = displayUsers[i];
202
+ const rowY = startY + i * (this.rowHeight + this.rowGap);
203
+ const avatarImg = avatarMap.get(user.userId) || null;
204
+
205
+ await drawUserRow(
206
+ ctx,
207
+ user,
208
+ this.padding,
209
+ rowY,
210
+ rowWidth,
211
+ this.rowHeight,
212
+ this.type,
213
+ avatarImg
214
+ );
215
+ }
216
+
217
+ // Draw footer
218
+ const footerY = startY + displayUsers.length * (this.rowHeight + this.rowGap);
219
+ this._drawFooter(ctx, footerY);
220
+
221
+ return canvas.toBuffer('image/png');
222
+ }
223
+
224
+ /**
225
+ * Render to JPEG format
226
+ * @param {number} quality - JPEG quality (0-100)
227
+ * @returns {Promise<Buffer>} JPEG image buffer
228
+ */
229
+ async renderJPEG(quality = 90) {
230
+ const height = this._calculateHeight();
231
+ const canvas = createCanvas(this.width, height);
232
+ const ctx = canvas.getContext('2d');
233
+
234
+ // Draw everything
235
+ this._drawBackground(ctx, height);
236
+ this._drawHeader(ctx);
237
+
238
+ const avatarMap = await this._loadAvatars();
239
+ const startY = this.padding + this.headerHeight;
240
+ const rowWidth = this.width - this.padding * 2;
241
+ const displayUsers = this.users.slice(0, this.rowCount);
242
+
243
+ for (let i = 0; i < displayUsers.length; i++) {
244
+ const user = displayUsers[i];
245
+ const rowY = startY + i * (this.rowHeight + this.rowGap);
246
+ const avatarImg = avatarMap.get(user.userId) || null;
247
+
248
+ await drawUserRow(ctx, user, this.padding, rowY, rowWidth, this.rowHeight, this.type, avatarImg);
249
+ }
250
+
251
+ const footerY = startY + displayUsers.length * (this.rowHeight + this.rowGap);
252
+ this._drawFooter(ctx, footerY);
253
+
254
+ return canvas.toBuffer('image/jpeg', { quality: quality / 100 });
255
+ }
256
+
257
+ /**
258
+ * Get the canvas dimensions
259
+ * @returns {object} Width and height
260
+ */
261
+ getDimensions() {
262
+ return {
263
+ width: this.width,
264
+ height: this._calculateHeight()
265
+ };
266
+ }
267
+ }
268
+
269
+ module.exports = { LeaderboardCard };