mirror of
https://hk.gh-proxy.com/https://github.com/NodeBB/nodebb-plugin-leaderboard.git
synced 2025-10-03 20:11:10 +08:00
add widget, add awaits
update for 4.x
This commit is contained in:
parent
f799d21a90
commit
47b8d08522
7 changed files with 3206 additions and 33 deletions
98
index.js
98
index.js
|
@ -2,29 +2,90 @@
|
|||
|
||||
const cron = require.main.require('cron').CronJob;
|
||||
const nconf = require.main.require('nconf');
|
||||
const winston = require.main.require('winston');
|
||||
|
||||
const controllersHelpers = require.main.require('./src/controllers/helpers');
|
||||
const usersController = require.main.require('./src/controllers/users');
|
||||
const db = require.main.require('./src/database');
|
||||
const privileges = require.main.require('./src/privileges');
|
||||
const user = require.main.require('./src/user');
|
||||
const pubsub = require.main.require('./src/pubsub');
|
||||
const helpers = require.main.require('./src/routes/helpers');
|
||||
|
||||
|
||||
const cronJobs = [];
|
||||
cronJobs.push(new cron('0 0 17 * * *', (() => { db.delete('users:reputation:daily'); }), null, false));
|
||||
cronJobs.push(new cron('0 0 17 * * 0', (() => { db.delete('users:reputation:weekly'); }), null, false));
|
||||
cronJobs.push(new cron('0 0 17 1 * *', (() => { db.delete('users:reputation:monthly'); }), null, false));
|
||||
cronJobs.push(new cron('0 0 17 * * *', (() => { deleteSet('users:reputation:daily'); }), null, false));
|
||||
cronJobs.push(new cron('0 0 17 * * 0', (() => { deleteSet('users:reputation:weekly'); }), null, false));
|
||||
cronJobs.push(new cron('0 0 17 1 * *', (() => { deleteSet('users:reputation:monthly'); }), null, false));
|
||||
|
||||
|
||||
const LeaderboardPlugin = {};
|
||||
async function deleteSet(set) {
|
||||
try {
|
||||
await db.delete(set);
|
||||
} catch (err) {
|
||||
winston.error(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
const LeaderboardPlugin = module.exports;
|
||||
|
||||
let app;
|
||||
|
||||
LeaderboardPlugin.init = async function (params) {
|
||||
helpers.setupPageRoute(params.router, '/leaderboard/:term?', params.middleware, [], LeaderboardPlugin.renderLeaderboard);
|
||||
app = params.app;
|
||||
helpers.setupPageRoute(params.router, '/leaderboard/:term?', params.middleware, [], LeaderboardPlugin.renderLeaderboardPage);
|
||||
reStartCronJobs();
|
||||
};
|
||||
|
||||
LeaderboardPlugin.renderLeaderboard = async function (req, res) {
|
||||
LeaderboardPlugin.defineWidgets = async (widgets) => {
|
||||
const widgetData = [
|
||||
{
|
||||
widget: 'leaderboard',
|
||||
name: 'Leaderboard',
|
||||
description: 'User leaderboard based on reputation',
|
||||
content: 'admin/partials/widgets/leaderboard.tpl',
|
||||
},
|
||||
];
|
||||
|
||||
await Promise.all(widgetData.map(async (widget) => {
|
||||
widget.content = await app.renderAsync(widget.content, {});
|
||||
}));
|
||||
|
||||
widgets = widgets.concat(widgetData);
|
||||
|
||||
return widgets;
|
||||
};
|
||||
|
||||
LeaderboardPlugin.renderLeaderboardWidget = async function (widget) {
|
||||
const numUsers = parseInt(widget.data.numUsers, 10) || 8;
|
||||
const term = widget.data.term || 'monthly';
|
||||
const set = `users:reputation:${term}`;
|
||||
const sidebarLocations = ['left', 'right', 'sidebar'];
|
||||
const userData = await user.getUsersFromSet(set, widget.uid, 0, numUsers - 1);
|
||||
const uids = userData.map(user => user && user.uid);
|
||||
const scores = await db.sortedSetScores(set, uids);
|
||||
const rankToColor = {
|
||||
0: 'gold',
|
||||
1: 'silver',
|
||||
2: 'sandybrown',
|
||||
};
|
||||
userData.forEach((user, index) => {
|
||||
if (user) {
|
||||
user.reputation = scores[index] || 0;
|
||||
user.rankColor = rankToColor[index] || '';
|
||||
user.rank = index + 1;
|
||||
}
|
||||
});
|
||||
widget.html = await app.renderAsync('widgets/leaderboard', {
|
||||
users: userData,
|
||||
sidebar: sidebarLocations.includes(widget.location),
|
||||
config: widget.templateData.config,
|
||||
relative_path: nconf.get('relative_path'),
|
||||
});
|
||||
return widget;
|
||||
};
|
||||
|
||||
LeaderboardPlugin.renderLeaderboardPage = async function (req, res) {
|
||||
const canView = await privileges.global.can('view:users', req.uid);
|
||||
if (!canView) {
|
||||
controllersHelpers.notAllowed(req, res);
|
||||
|
@ -62,6 +123,7 @@ LeaderboardPlugin.renderLeaderboard = async function (req, res) {
|
|||
|
||||
userData.breadcrumbs = controllersHelpers.buildBreadcrumbs(breadcrumbs);
|
||||
userData['section_sort-reputation'] = true;
|
||||
userData.section_joindate = false;
|
||||
userData.title = '[[leaderboard:leaderboard]]';
|
||||
|
||||
res.render('leaderboard', userData);
|
||||
|
@ -81,41 +143,43 @@ LeaderboardPlugin.getNavigation = async function (core) {
|
|||
return core;
|
||||
};
|
||||
|
||||
LeaderboardPlugin.onUpvote = function (data) {
|
||||
LeaderboardPlugin.onUpvote = async function (data) {
|
||||
let change = 0;
|
||||
if (data.current === 'unvote') {
|
||||
change = 1;
|
||||
} else if (data.current === 'downvote') {
|
||||
change = 2;
|
||||
}
|
||||
updateLeaderboards(change, data.owner);
|
||||
await updateLeaderboards(change, data.owner);
|
||||
};
|
||||
|
||||
LeaderboardPlugin.onDownvote = function (data) {
|
||||
LeaderboardPlugin.onDownvote = async function (data) {
|
||||
let change = 0;
|
||||
if (data.current === 'unvote') {
|
||||
change = -1;
|
||||
} else if (data.current === 'upvote') {
|
||||
change = -2;
|
||||
}
|
||||
updateLeaderboards(change, data.owner);
|
||||
await updateLeaderboards(change, data.owner);
|
||||
};
|
||||
|
||||
LeaderboardPlugin.onUnvote = function (data) {
|
||||
LeaderboardPlugin.onUnvote = async function (data) {
|
||||
let change = 0;
|
||||
if (data.current === 'upvote') {
|
||||
change = -1;
|
||||
} else if (data.current === 'downvote') {
|
||||
change = 1;
|
||||
}
|
||||
updateLeaderboards(change, data.owner);
|
||||
await updateLeaderboards(change, data.owner);
|
||||
};
|
||||
|
||||
function updateLeaderboards(change, owner) {
|
||||
async function updateLeaderboards(change, owner) {
|
||||
if (change) {
|
||||
db.sortedSetIncrBy('users:reputation:daily', change, owner);
|
||||
db.sortedSetIncrBy('users:reputation:weekly', change, owner);
|
||||
db.sortedSetIncrBy('users:reputation:monthly', change, owner);
|
||||
await Promise.all([
|
||||
db.sortedSetIncrBy('users:reputation:daily', change, owner),
|
||||
db.sortedSetIncrBy('users:reputation:weekly', change, owner),
|
||||
db.sortedSetIncrBy('users:reputation:monthly', change, owner),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,5 +209,3 @@ function stopCronJobs() {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LeaderboardPlugin;
|
||||
|
|
3069
package-lock.json
generated
Normal file
3069
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -20,7 +20,7 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"nbbpm": {
|
||||
"compatibility": "^1.14.1"
|
||||
"compatibility": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0",
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
{ "hook": "action:post.downvote", "method": "onDownvote"},
|
||||
{ "hook": "action:post.unvote", "method": "onUnvote"},
|
||||
{ "hook": "action:plugin.deactivate", "method": "deactivate"},
|
||||
{ "hook": "filter:navigation.available", "method": "getNavigation"}
|
||||
{ "hook": "filter:navigation.available", "method": "getNavigation"},
|
||||
{ "hook": "filter:widgets.getWidgets", "method": "defineWidgets" },
|
||||
{ "hook": "filter:widget.render:leaderboard", "method": "renderLeaderboardWidget" }
|
||||
],
|
||||
"templates": "templates",
|
||||
"languages": "languages"
|
||||
|
|
12
templates/admin/partials/widgets/leaderboard.tpl
Normal file
12
templates/admin/partials/widgets/leaderboard.tpl
Normal file
|
@ -0,0 +1,12 @@
|
|||
<div class="mb-3">
|
||||
<label class="form-label">Amount of Users to display:</label>
|
||||
<input type="text" class="form-control" name="numUsers" placeholder="4" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Leaderboard Term:</label>
|
||||
<select class="form-select" name="term">
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="daily">Daily</option>
|
||||
</select>
|
||||
</div>
|
|
@ -1,18 +1,25 @@
|
|||
|
||||
<div data-widget-area="header">
|
||||
{{{each widgets.header}}}
|
||||
{{widgets.header.html}}
|
||||
{{{end}}}
|
||||
</div>
|
||||
<div class="users">
|
||||
|
||||
<!-- IMPORT partials/breadcrumbs.tpl -->
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="<!-- IF daily -->active<!-- ENDIF daily -->"><a href='{config.relative_path}/leaderboard/daily'>[[recent:day]]</a></li>
|
||||
<li class="<!-- IF weekly -->active<!-- ENDIF weekly -->"><a href='{config.relative_path}/leaderboard/weekly'>[[recent:week]]</a></li>
|
||||
<li class="<!-- IF monthly -->active<!-- ENDIF monthly -->"><a href='{config.relative_path}/leaderboard/monthly'>[[recent:month]]</a></li>
|
||||
</ul>
|
||||
<h3 class="fw-semibold">[[global:users]]</h3>
|
||||
<div class="d-flex flex-wrap justify-content-between">
|
||||
<div class="mb-2 mb-md-0">
|
||||
<div component="user/list/menu" class="text-sm d-flex flex-wrap align-items-center gap-2">
|
||||
<a class="btn btn-ghost btn-sm ff-secondary fw-semibold {{{ if daily }}}active{{{ end }}}" href="{config.relative_path}/leaderboard/daily">[[recent:day]]</a>
|
||||
<a class="btn btn-ghost btn-sm ff-secondary fw-semibold {{{ if weekly }}}active{{{ end }}}" href="{config.relative_path}/leaderboard/weekly">[[recent:week]]</a>
|
||||
<a class="btn btn-ghost btn-sm ff-secondary fw-semibold {{{ if monthly }}}active{{{ end }}}" href="{config.relative_path}/leaderboard/monthly">[[recent:month]]</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
<ul id="users-container" class="users-container">
|
||||
<!-- IMPORT partials/users_list.tpl -->
|
||||
</ul>
|
||||
<div id="users-container" class="users-container row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-4">
|
||||
{{{ each users }}}
|
||||
<!-- IMPORT partials/users/item.tpl -->
|
||||
{{{ end }}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
21
templates/widgets/leaderboard.tpl
Normal file
21
templates/widgets/leaderboard.tpl
Normal file
|
@ -0,0 +1,21 @@
|
|||
<div class="{{{ if sidebar }}}row row-cols-1 px-3{{{ else }}}row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-4{{{ end }}} mb-2">
|
||||
{{{ each users }}}
|
||||
<a href="{config.relative_path}/user/{./userslug}" class="btn btn-ghost d-flex gap-2 ff-secondary align-items-start text-start p-2 ff-base">
|
||||
<div class="position-relative {{{ if !./rankColor}}}invisible{{{ end }}}">
|
||||
<i class="fa-solid fa-trophy fa-2x" style="color: {./rankColor};"></i>
|
||||
<span style="width:18px; height:18px; text-align: center;" class="mt-1 d-inline-block rounded-circle lh-1 position-absolute top-0 start-50 translate-middle-x fw-bold ff-secondary">{./rank}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex flex-column gap-1 text-truncate">
|
||||
<div class="d-flex gap-2">
|
||||
<div>{buildAvatar(@value, "24px", true, "flex-shrink-0")}</div>
|
||||
<div class="fw-semibold text-truncate" title="{./displayname}">{./displayname}</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted text-truncate">
|
||||
{./reputation}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{{{ end }}}
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue