hrvclan 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/package.json +33 -0
- package/readme.md +46 -0
- package/src/index.js +104 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hrvclan",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Generate leaderboard card images using hrv-web.netlify.app API",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"leaderboard",
|
|
9
|
+
"card",
|
|
10
|
+
"image-generator",
|
|
11
|
+
"hrv-web",
|
|
12
|
+
"api"
|
|
13
|
+
],
|
|
14
|
+
"author": "Nguyễn Thành Tài",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"axios": "^1.13.5",
|
|
18
|
+
"node-fetch": "^3.3.2",
|
|
19
|
+
"puppeteer": "^24.37.2"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^25.2.2",
|
|
23
|
+
"typescript": "^5.5.4"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src/**/*",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/nthanhtai08/hrvclan.git"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# 📊 Leaderboard HTML → PNG Renderer (CommonJS)
|
|
2
|
+
|
|
3
|
+
Module Node.js sử dụng **Puppeteer** để:
|
|
4
|
+
|
|
5
|
+
- Encode dữ liệu leaderboard
|
|
6
|
+
- Gửi lên API HTML
|
|
7
|
+
- Render trang HTML
|
|
8
|
+
- Chụp màn hình thành ảnh PNG
|
|
9
|
+
- Lưu file về máy
|
|
10
|
+
|
|
11
|
+
Hoạt động với **CommonJS (`require`)**, không dùng ESModule.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# 🚀 1. Yêu cầu
|
|
16
|
+
|
|
17
|
+
- Node.js >= 18
|
|
18
|
+
- Không dùng `"type": "module"` trong `package.json`
|
|
19
|
+
- Windows / Linux / VPS đều chạy được
|
|
20
|
+
|
|
21
|
+
Kiểm tra Node:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node -v
|
|
25
|
+
```
|
|
26
|
+
# 2. How to use
|
|
27
|
+
```
|
|
28
|
+
## Bước 1: Cài Module
|
|
29
|
+
```npm install @nthanhtai08/hrvclan```
|
|
30
|
+
## Bước 2: Import Package
|
|
31
|
+
```
|
|
32
|
+
const {
|
|
33
|
+
getLeaderboardCardUrl,
|
|
34
|
+
fetchLeaderboardCardImage,
|
|
35
|
+
saveLeaderboardImage
|
|
36
|
+
} = require('hrvclan');
|
|
37
|
+
```
|
|
38
|
+
## Bước 3: Done
|
|
39
|
+
```
|
|
40
|
+
async function run() {
|
|
41
|
+
await saveLeaderboardImage(entries, "./leaderboard.png");
|
|
42
|
+
console.log("✅ Done");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
run();
|
|
46
|
+
```
|
package/src/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const puppeteer = require('puppeteer');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Encode base64url
|
|
6
|
+
*/
|
|
7
|
+
function toBase64Url(input) {
|
|
8
|
+
return Buffer
|
|
9
|
+
.from(input, 'utf8')
|
|
10
|
+
.toString('base64')
|
|
11
|
+
.replace(/\+/g, '-')
|
|
12
|
+
.replace(/\//g, '_')
|
|
13
|
+
.replace(/=+$/, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validate entries
|
|
18
|
+
*/
|
|
19
|
+
function validateEntries(entries) {
|
|
20
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
21
|
+
throw new Error('Entries phải là mảng và có ít nhất 1 phần tử');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
entries.forEach((entry, index) => {
|
|
25
|
+
if (!entry.name || typeof entry.name !== 'string') {
|
|
26
|
+
throw new Error(`Entry ${index + 1}: name không hợp lệ`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof entry.score !== 'number' || !Number.isFinite(entry.score)) {
|
|
30
|
+
throw new Error(`Entry ${index + 1}: score không hợp lệ`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof entry.avatar !== 'string') {
|
|
34
|
+
throw new Error(`Entry ${index + 1}: avatar phải là string`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Tạo URL
|
|
41
|
+
*/
|
|
42
|
+
function getLeaderboardCardUrl(entries) {
|
|
43
|
+
validateEntries(entries);
|
|
44
|
+
|
|
45
|
+
const encoded = toBase64Url(JSON.stringify(entries));
|
|
46
|
+
|
|
47
|
+
return `https://hrv-web.netlify.app/apis/leaderboard-card?data=${encoded}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fetchLeaderboardCardImage(entries) {
|
|
51
|
+
const url = getLeaderboardCardUrl(entries);
|
|
52
|
+
|
|
53
|
+
const browser = await puppeteer.launch({
|
|
54
|
+
headless: true,
|
|
55
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const page = await browser.newPage();
|
|
60
|
+
|
|
61
|
+
// Viewport dọc 9:16
|
|
62
|
+
await page.setViewport({
|
|
63
|
+
width: 1080,
|
|
64
|
+
height: 1920,
|
|
65
|
+
deviceScaleFactor: 2
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await page.goto(url, {
|
|
69
|
+
waitUntil: 'networkidle0',
|
|
70
|
+
timeout: 30000
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Đợi container render xong
|
|
74
|
+
await page.waitForSelector('.container');
|
|
75
|
+
|
|
76
|
+
const element = await page.$('.container');
|
|
77
|
+
|
|
78
|
+
if (!element) throw new Error('Không tìm thấy .container');
|
|
79
|
+
|
|
80
|
+
const screenshot = await element.screenshot({
|
|
81
|
+
type: 'png'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return screenshot;
|
|
85
|
+
|
|
86
|
+
} finally {
|
|
87
|
+
await browser.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async function saveLeaderboardImage(entries, filePath) {
|
|
93
|
+
const buffer = await fetchLeaderboardCardImage(entries);
|
|
94
|
+
fs.writeFileSync(filePath, buffer);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Export CommonJS
|
|
99
|
+
*/
|
|
100
|
+
module.exports = {
|
|
101
|
+
getLeaderboardCardUrl,
|
|
102
|
+
fetchLeaderboardCardImage,
|
|
103
|
+
saveLeaderboardImage
|
|
104
|
+
};
|