add widget, add awaits

update for 4.x
This commit is contained in:
Barış Soner Uşaklı 2025-02-13 11:58:06 -05:00
parent f799d21a90
commit 47b8d08522
7 changed files with 3206 additions and 33 deletions

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@
"license": "MIT",
"dependencies": {},
"nbbpm": {
"compatibility": "^1.14.1"
"compatibility": "^4.0.0"
},
"devDependencies": {
"eslint": "^7.32.0",

View file

@ -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"

View 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>

View file

@ -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>

View 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>