nodebb-plugin-niki-loyalty 1.0.1
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/.gitattributes +22 -0
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/commitlint.config.js +26 -0
- package/eslint.config.mjs +10 -0
- package/languages/de/quickstart.json +3 -0
- package/languages/en-GB/quickstart.json +3 -0
- package/languages/en-US/quickstart.json +3 -0
- package/lib/controllers.js +20 -0
- package/library.js +94 -0
- package/package.json +30 -0
- package/plugin.json +20 -0
- package/public/lib/acp-main.js +18 -0
- package/public/lib/admin.js +55 -0
- package/public/lib/main.js +32 -0
- package/public/lib/quickstart.js +15 -0
- package/renovate.json +5 -0
- package/scss/quickstart.scss +1 -0
- package/static/lib/client.js +82 -0
- package/static/samplefile.html +5 -0
- package/static/style.css +206 -0
- package/templates/admin/plugins/quickstart/partials/sorted-list/form.tpl +10 -0
- package/templates/admin/plugins/quickstart/partials/sorted-list/item.tpl +12 -0
- package/templates/admin/plugins/quickstart.tpl +68 -0
- package/templates/niki-wallet.tpl +35 -0
- package/templates/quickstart.tpl +7 -0
- package/test/.eslintrc +9 -0
- package/test/index.js +41 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Auto detect text files and perform LF normalization
|
|
2
|
+
* text=auto
|
|
3
|
+
|
|
4
|
+
# Custom for Visual Studio
|
|
5
|
+
*.cs diff=csharp
|
|
6
|
+
*.sln merge=union
|
|
7
|
+
*.csproj merge=union
|
|
8
|
+
*.vbproj merge=union
|
|
9
|
+
*.fsproj merge=union
|
|
10
|
+
*.dbproj merge=union
|
|
11
|
+
|
|
12
|
+
# Standard to msysgit
|
|
13
|
+
*.doc diff=astextplain
|
|
14
|
+
*.DOC diff=astextplain
|
|
15
|
+
*.docx diff=astextplain
|
|
16
|
+
*.DOCX diff=astextplain
|
|
17
|
+
*.dot diff=astextplain
|
|
18
|
+
*.DOT diff=astextplain
|
|
19
|
+
*.pdf diff=astextplain
|
|
20
|
+
*.PDF diff=astextplain
|
|
21
|
+
*.rtf diff=astextplain
|
|
22
|
+
*.RTF diff=astextplain
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 NodeBB Inc. <sales@nodebb.org>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Quickstart Plugin for NodeBB
|
|
2
|
+
|
|
3
|
+
A starter kit for quickly creating NodeBB plugins. Comes with a pre-setup SCSS file, server side JS script with an `static:app.load` hook, and a client-side script. Most plugins need at least one of the above, so this ought to save you some time. For a full list of hooks have a look at our [wiki page](https://github.com/NodeBB/NodeBB/wiki/Hooks), and for more information about creating plugins please visit our [documentation portal](https://docs.nodebb.org/).
|
|
4
|
+
|
|
5
|
+
Fork this or copy it, and using your favourite text editor find and replace all instances of `nodebb-plugin-quickstart` with `nodebb-plugin-your-plugins-name`. Change the author's name in the LICENSE and package.json files.
|
|
6
|
+
|
|
7
|
+
## Hello World
|
|
8
|
+
|
|
9
|
+
Really simple, just edit `public/lib/main.js` and paste in `console.log('hello world');`, and that's it!
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
npm install nodebb-plugin-quickstart
|
|
14
|
+
|
|
15
|
+
## Screenshots
|
|
16
|
+
|
|
17
|
+
Don't forget to add screenshots!
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
extends: ['@commitlint/config-angular'],
|
|
5
|
+
rules: {
|
|
6
|
+
'header-max-length': [1, 'always', 72],
|
|
7
|
+
'type-enum': [
|
|
8
|
+
2,
|
|
9
|
+
'always',
|
|
10
|
+
[
|
|
11
|
+
'breaking',
|
|
12
|
+
'build',
|
|
13
|
+
'chore',
|
|
14
|
+
'ci',
|
|
15
|
+
'docs',
|
|
16
|
+
'feat',
|
|
17
|
+
'fix',
|
|
18
|
+
'perf',
|
|
19
|
+
'refactor',
|
|
20
|
+
'revert',
|
|
21
|
+
'style',
|
|
22
|
+
'test',
|
|
23
|
+
],
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Controllers = module.exports;
|
|
4
|
+
|
|
5
|
+
Controllers.renderAdminPage = function (req, res/* , next */) {
|
|
6
|
+
/*
|
|
7
|
+
Make sure the route matches your path to template exactly.
|
|
8
|
+
|
|
9
|
+
If your route was:
|
|
10
|
+
myforum.com/some/complex/route
|
|
11
|
+
your template should be:
|
|
12
|
+
templates/some/complex/route.tpl
|
|
13
|
+
and you would render it like so:
|
|
14
|
+
res.render('some/complex/route');
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
res.render('admin/plugins/quickstart', {
|
|
18
|
+
title: 'Quick Start',
|
|
19
|
+
});
|
|
20
|
+
};
|
package/library.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require.main.require('./src/database');
|
|
4
|
+
const user = require.main.require('./src/user');
|
|
5
|
+
const routeHelpers = require.main.require('./src/controllers/helpers');
|
|
6
|
+
|
|
7
|
+
const Plugin = {};
|
|
8
|
+
|
|
9
|
+
// --- AYARLAR ---
|
|
10
|
+
const SETTINGS = {
|
|
11
|
+
pointsPerHeartbeat: 5, // Her vuruşta kaç puan?
|
|
12
|
+
heartbeatInterval: 60, // Kaç saniyede bir (Client ile uyumlu olmalı)
|
|
13
|
+
dailyCap: 250 // Günlük maksimum puan (Örn: 50dk aktiflik)
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
Plugin.init = async function (params) {
|
|
17
|
+
const router = params.router;
|
|
18
|
+
const middleware = params.middleware;
|
|
19
|
+
|
|
20
|
+
// API: Kalp Atışı (Puan Kazanma)
|
|
21
|
+
router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
|
|
22
|
+
const uid = req.uid;
|
|
23
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // 20251214
|
|
24
|
+
|
|
25
|
+
// 1. Günlük Limiti Kontrol Et
|
|
26
|
+
const dailyKey = `niki:daily:${uid}:${today}`;
|
|
27
|
+
const currentDailyScore = await db.getObjectField(dailyKey, 'score') || 0;
|
|
28
|
+
|
|
29
|
+
if (parseInt(currentDailyScore) >= SETTINGS.dailyCap) {
|
|
30
|
+
return res.json({ earned: false, reason: 'daily_cap' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Puan Ver
|
|
34
|
+
await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
|
|
35
|
+
await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
|
|
36
|
+
|
|
37
|
+
// 3. Güncel Bakiyeyi Dön
|
|
38
|
+
const newBalance = await user.getUserField(uid, 'niki_points');
|
|
39
|
+
return res.json({
|
|
40
|
+
earned: true,
|
|
41
|
+
points: SETTINGS.pointsPerHeartbeat,
|
|
42
|
+
total: newBalance,
|
|
43
|
+
daily: parseInt(currentDailyScore) + SETTINGS.pointsPerHeartbeat
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// SAYFA: Cüzdan (/niki-wallet)
|
|
48
|
+
routeHelpers.setupPageRoute(router, '/niki-wallet', middleware, [], async (req, res) => {
|
|
49
|
+
const uid = req.uid;
|
|
50
|
+
if (!uid) return res.redirect('/login');
|
|
51
|
+
|
|
52
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
53
|
+
|
|
54
|
+
// Verileri Çek
|
|
55
|
+
const [userData, dailyData] = await Promise.all([
|
|
56
|
+
user.getUserFields(uid, ['username', 'userslug', 'picture', 'niki_points']),
|
|
57
|
+
db.getObject(`niki:daily:${uid}:${today}`)
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const currentPoints = parseInt(userData.niki_points) || 0;
|
|
61
|
+
const dailyScore = parseInt(dailyData ? dailyData.score : 0) || 0;
|
|
62
|
+
|
|
63
|
+
// Yüzdelik Hesapla (Bar için)
|
|
64
|
+
let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
|
|
65
|
+
if (dailyPercent > 100) dailyPercent = 100;
|
|
66
|
+
|
|
67
|
+
res.render('niki-wallet', {
|
|
68
|
+
title: 'Niki Cüzdan',
|
|
69
|
+
points: currentPoints,
|
|
70
|
+
dailyScore: dailyScore,
|
|
71
|
+
dailyCap: SETTINGS.dailyCap,
|
|
72
|
+
dailyPercent: dailyPercent,
|
|
73
|
+
user: userData
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
Plugin.addScripts = async function (scripts) {
|
|
79
|
+
scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
|
|
80
|
+
return scripts;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
Plugin.addNavigation = async function (nav) {
|
|
84
|
+
nav.push({
|
|
85
|
+
"route": "/niki-wallet",
|
|
86
|
+
"title": "Niki Cüzdan",
|
|
87
|
+
"enabled": true,
|
|
88
|
+
"iconClass": "fa-coffee",
|
|
89
|
+
"text": "Niki Cüzdan"
|
|
90
|
+
});
|
|
91
|
+
return nav;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
module.exports = Plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodebb-plugin-niki-loyalty",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
|
|
5
|
+
"main": "library.js",
|
|
6
|
+
"nbbpm": {
|
|
7
|
+
"compatibility": "^4.0.0"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"nodebb",
|
|
11
|
+
"plugin",
|
|
12
|
+
"loyalty",
|
|
13
|
+
"gamification",
|
|
14
|
+
"points",
|
|
15
|
+
"niki"
|
|
16
|
+
],
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "IEU Forum Team",
|
|
19
|
+
"email": "admin@ieu.app"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/sucreistaken/nodebb-plugin-niki-loyalty"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/sucreistaken/nodebb-plugin-niki-loyalty/issues"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {}
|
|
30
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nodebb-plugin-niki-loyalty",
|
|
3
|
+
"name": "Niki Loyalty System",
|
|
4
|
+
"description": "Öğrenciler ders çalışsın, kahve kazansın.",
|
|
5
|
+
"url": "https://forum.ieu.app",
|
|
6
|
+
"library": "./library.js",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{ "hook": "static:app.load", "method": "init" },
|
|
9
|
+
{ "hook": "filter:scripts.get", "method": "addScripts" },
|
|
10
|
+
{ "hook": "filter:navigation.available", "method": "addNavigation" },
|
|
11
|
+
{ "hook": "action:topic.get", "method": "checkTopicVisit" }
|
|
12
|
+
],
|
|
13
|
+
"scripts": [
|
|
14
|
+
"static/lib/client.js"
|
|
15
|
+
],
|
|
16
|
+
"staticDirs": {
|
|
17
|
+
"static": "./static"
|
|
18
|
+
},
|
|
19
|
+
"templates": "./templates"
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
$(document).ready(function () {
|
|
4
|
+
/*
|
|
5
|
+
This file shows how admin page client-side javascript can be included via a plugin.
|
|
6
|
+
If you check `plugin.json`, you'll see that this file is listed under "acpScripts".
|
|
7
|
+
That array tells NodeBB which files to bundle into the minified javascript
|
|
8
|
+
that is served to the end user.
|
|
9
|
+
|
|
10
|
+
Some events you can elect to listen for:
|
|
11
|
+
|
|
12
|
+
$(document).ready(); Fired when the DOM is ready
|
|
13
|
+
$(window).on('action:ajaxify.end', function(data) { ... }); "data" contains "url"
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
console.log('nodebb-plugin-quickstart: acp-loaded');
|
|
17
|
+
// Note how this is shown in the console on the first load of every page in the ACP
|
|
18
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
This file is located in the "modules" block of plugin.json
|
|
5
|
+
It is only loaded when the user navigates to /admin/plugins/quickstart page
|
|
6
|
+
It is not bundled into the min file that is served on the first load of the page.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { save, load } from 'settings';
|
|
10
|
+
import * as uploader from 'uploader';
|
|
11
|
+
|
|
12
|
+
export function init() {
|
|
13
|
+
handleSettingsForm();
|
|
14
|
+
setupUploader();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function handleSettingsForm() {
|
|
18
|
+
load('quickstart', $('.quickstart-settings'), function () {
|
|
19
|
+
setupColorInputs();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
$('#save').on('click', () => {
|
|
23
|
+
save('quickstart', $('.quickstart-settings')); // pass in a function in the 3rd parameter to override the default success/failure handler
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setupColorInputs() {
|
|
28
|
+
var colorInputs = $('[data-settings="colorpicker"]');
|
|
29
|
+
colorInputs.on('change', updateColors);
|
|
30
|
+
updateColors();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function updateColors() {
|
|
34
|
+
$('#preview').css({
|
|
35
|
+
color: $('#color').val(),
|
|
36
|
+
'background-color': $('#bgColor').val(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setupUploader() {
|
|
41
|
+
$('#content input[data-action="upload"]').each(function () {
|
|
42
|
+
var uploadBtn = $(this);
|
|
43
|
+
uploadBtn.on('click', function () {
|
|
44
|
+
uploader.show({
|
|
45
|
+
route: config.relative_path + '/api/admin/upload/file',
|
|
46
|
+
params: {
|
|
47
|
+
folder: 'quickstart',
|
|
48
|
+
},
|
|
49
|
+
accept: 'image/*',
|
|
50
|
+
}, function (image) {
|
|
51
|
+
$('#' + uploadBtn.attr('data-target')).val(image);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This file shows how client-side javascript can be included via a plugin.
|
|
5
|
+
* If you check `plugin.json`, you'll see that this file is listed under "scripts".
|
|
6
|
+
* That array tells NodeBB which files to bundle into the minified javascript
|
|
7
|
+
* that is served to the end user.
|
|
8
|
+
*
|
|
9
|
+
* There are two (standard) ways to wait for when NodeBB is ready.
|
|
10
|
+
* This one below executes when NodeBB reports it is ready...
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
(async () => {
|
|
14
|
+
const hooks = await app.require('hooks');
|
|
15
|
+
|
|
16
|
+
hooks.on('action:app.load', () => {
|
|
17
|
+
// called once when nbb has loaded
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
hooks.on('action:ajaxify.end', (/* data */) => {
|
|
21
|
+
// called everytime user navigates between pages including first load
|
|
22
|
+
});
|
|
23
|
+
})();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ... and this one reports when the DOM is loaded (but NodeBB might not be fully ready yet).
|
|
27
|
+
* For most cases, you'll want the one above.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
$(document).ready(function () {
|
|
31
|
+
// ...
|
|
32
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
This file is located in the "modules" block of plugin.json
|
|
5
|
+
It is only loaded when the user navigates to /quickstart page
|
|
6
|
+
It is not bundled into the min file that is served on the first load of the page.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
define('forum/quickstart', function () {
|
|
10
|
+
var module = {};
|
|
11
|
+
module.init = function () {
|
|
12
|
+
$('#last-p').text('quickstart.js loaded!');
|
|
13
|
+
};
|
|
14
|
+
return module;
|
|
15
|
+
});
|
package/renovate.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Place any SASS in here */
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
$(document).ready(function () {
|
|
4
|
+
// --- NIKI LOGO URL ---
|
|
5
|
+
// Buraya Niki'nin transparent PNG logosunu koymalısın.
|
|
6
|
+
const NIKI_LOGO_URL = 'https://i.imgur.com/kXUe4M6.png'; // Örnek kedi logosu
|
|
7
|
+
|
|
8
|
+
// 1. Widget'ı Ekrana Bas (Eğer giriş yapmışsa)
|
|
9
|
+
if (app.user.uid > 0 && $('#niki-floating-widget').length === 0) {
|
|
10
|
+
const widgetHtml = `
|
|
11
|
+
<div id="niki-floating-widget">
|
|
12
|
+
<div class="niki-widget-content" onclick="ajaxify.go('niki-wallet')">
|
|
13
|
+
<img src="${NIKI_LOGO_URL}" class="niki-widget-logo" alt="Niki">
|
|
14
|
+
<div class="niki-widget-text">
|
|
15
|
+
<span class="niki-lbl">PUANIM</span>
|
|
16
|
+
<span class="niki-val" id="niki-live-points">...</span>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
`;
|
|
21
|
+
$('body').append(widgetHtml);
|
|
22
|
+
|
|
23
|
+
// Açılışta puanı çek
|
|
24
|
+
$.get('/api/user/' + app.user.userslug, function(data) {
|
|
25
|
+
$('#niki-live-points').text(data.niki_points || 0);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Aktiflik Takibi (Time-on-Site)
|
|
30
|
+
let activeSeconds = 0;
|
|
31
|
+
let isUserActive = false;
|
|
32
|
+
let idleTimer;
|
|
33
|
+
|
|
34
|
+
// Hareket algılayıcı
|
|
35
|
+
function resetIdleTimer() {
|
|
36
|
+
isUserActive = true;
|
|
37
|
+
clearTimeout(idleTimer);
|
|
38
|
+
idleTimer = setTimeout(() => { isUserActive = false; }, 30000); // 30sn hareketsizse dur
|
|
39
|
+
}
|
|
40
|
+
$(window).on('mousemove scroll keydown click', resetIdleTimer);
|
|
41
|
+
|
|
42
|
+
// Her saniye kontrol
|
|
43
|
+
setInterval(() => {
|
|
44
|
+
// Sadece Topic sayfalarındaysak say
|
|
45
|
+
if (ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
|
|
46
|
+
activeSeconds++;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 60 saniye doldu mu?
|
|
50
|
+
if (activeSeconds >= 60) {
|
|
51
|
+
sendHeartbeat();
|
|
52
|
+
activeSeconds = 0;
|
|
53
|
+
}
|
|
54
|
+
}, 1000);
|
|
55
|
+
|
|
56
|
+
function sendHeartbeat() {
|
|
57
|
+
const topicId = ajaxify.data.tid;
|
|
58
|
+
$.post('/api/niki-loyalty/heartbeat', { tid: topicId, _csrf: config.csrf_token }, function(res) {
|
|
59
|
+
if (res.earned) {
|
|
60
|
+
// UI Güncelle
|
|
61
|
+
$('#niki-live-points').text(res.total);
|
|
62
|
+
showNikiToast(`+${res.points} Puan! ☕`);
|
|
63
|
+
|
|
64
|
+
// Widget'a ufak bir "bounce" efekti ver
|
|
65
|
+
$('#niki-floating-widget').addClass('niki-bounce');
|
|
66
|
+
setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Özel Bildirim (Toast)
|
|
72
|
+
function showNikiToast(msg) {
|
|
73
|
+
$('.niki-toast').remove();
|
|
74
|
+
const toast = $(`<div class="niki-toast"><i class="fa fa-paw"></i> ${msg}</div>`);
|
|
75
|
+
$('body').append(toast);
|
|
76
|
+
setTimeout(() => { toast.addClass('show'); }, 100);
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
toast.removeClass('show');
|
|
79
|
+
setTimeout(() => toast.remove(), 300);
|
|
80
|
+
}, 3000);
|
|
81
|
+
}
|
|
82
|
+
});
|
package/static/style.css
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/* --- NIKI LOYALTY UI --- */
|
|
2
|
+
|
|
3
|
+
/* 1. Floating Widget (Sol Alt) */
|
|
4
|
+
#niki-floating-widget {
|
|
5
|
+
position: fixed;
|
|
6
|
+
bottom: 25px;
|
|
7
|
+
left: 25px;
|
|
8
|
+
z-index: 1050;
|
|
9
|
+
font-family: 'Poppins', sans-serif; /* Varsa sitenin fontu */
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.niki-widget-content {
|
|
13
|
+
background: rgba(255, 255, 255, 0.9);
|
|
14
|
+
backdrop-filter: blur(10px);
|
|
15
|
+
border: 1px solid rgba(78, 52, 46, 0.1);
|
|
16
|
+
padding: 6px 16px 6px 6px;
|
|
17
|
+
border-radius: 40px;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 10px;
|
|
21
|
+
box-shadow: 0 8px 20px rgba(78, 52, 46, 0.15);
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.niki-widget-content:hover {
|
|
27
|
+
transform: translateY(-3px);
|
|
28
|
+
box-shadow: 0 12px 25px rgba(78, 52, 46, 0.25);
|
|
29
|
+
background: #fff;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.niki-widget-logo {
|
|
33
|
+
width: 36px;
|
|
34
|
+
height: 36px;
|
|
35
|
+
border-radius: 50%;
|
|
36
|
+
background: #EFEBE9;
|
|
37
|
+
padding: 2px;
|
|
38
|
+
object-fit: cover;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.niki-widget-text {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
line-height: 1.1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.niki-lbl {
|
|
48
|
+
font-size: 9px;
|
|
49
|
+
font-weight: 700;
|
|
50
|
+
color: #8D6E63;
|
|
51
|
+
letter-spacing: 0.5px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.niki-val {
|
|
55
|
+
font-size: 15px;
|
|
56
|
+
font-weight: 800;
|
|
57
|
+
color: #4E342E;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Animasyon: Bounce */
|
|
61
|
+
.niki-bounce {
|
|
62
|
+
animation: niki-bounce-anim 0.5s;
|
|
63
|
+
}
|
|
64
|
+
@keyframes niki-bounce-anim {
|
|
65
|
+
0%, 100% { transform: scale(1); }
|
|
66
|
+
50% { transform: scale(1.15); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* 2. Toast Bildirimi */
|
|
70
|
+
.niki-toast {
|
|
71
|
+
position: fixed;
|
|
72
|
+
bottom: 90px;
|
|
73
|
+
left: 25px;
|
|
74
|
+
background: #4E342E;
|
|
75
|
+
color: #fff;
|
|
76
|
+
padding: 10px 20px;
|
|
77
|
+
border-radius: 12px;
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 8px;
|
|
84
|
+
z-index: 1060;
|
|
85
|
+
opacity: 0;
|
|
86
|
+
transform: translateY(20px) scale(0.9);
|
|
87
|
+
transition: all 0.3s ease;
|
|
88
|
+
pointer-events: none;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.niki-toast.show {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
transform: translateY(0) scale(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* 3. Cüzdan Sayfası */
|
|
97
|
+
.niki-wallet-wrapper {
|
|
98
|
+
max-width: 450px;
|
|
99
|
+
margin: 40px auto;
|
|
100
|
+
background: #fff;
|
|
101
|
+
border-radius: 24px;
|
|
102
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.08);
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
text-align: center;
|
|
105
|
+
position: relative;
|
|
106
|
+
border: 1px solid #f0f0f0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.niki-header-bg {
|
|
110
|
+
background: #4E342E;
|
|
111
|
+
height: 120px;
|
|
112
|
+
width: 100%;
|
|
113
|
+
position: absolute;
|
|
114
|
+
top: 0;
|
|
115
|
+
left: 0;
|
|
116
|
+
z-index: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.niki-wallet-content {
|
|
120
|
+
position: relative;
|
|
121
|
+
z-index: 1;
|
|
122
|
+
padding: 30px;
|
|
123
|
+
padding-top: 60px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.niki-wallet-avatar {
|
|
127
|
+
width: 110px;
|
|
128
|
+
height: 110px;
|
|
129
|
+
border-radius: 50%;
|
|
130
|
+
border: 5px solid #fff;
|
|
131
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
|
132
|
+
margin: 0 auto 20px;
|
|
133
|
+
background: #fff;
|
|
134
|
+
display: flex; /* Logo ortalama */
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.niki-wallet-avatar img {
|
|
140
|
+
width: 100%;
|
|
141
|
+
height: 100%;
|
|
142
|
+
border-radius: 50%;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.niki-balance-big {
|
|
146
|
+
font-size: 42px;
|
|
147
|
+
font-weight: 900;
|
|
148
|
+
color: #4E342E;
|
|
149
|
+
margin-bottom: 5px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.niki-balance-label {
|
|
153
|
+
color: #8D6E63;
|
|
154
|
+
font-size: 14px;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
letter-spacing: 1px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Progress Bar */
|
|
161
|
+
.niki-daily-stats {
|
|
162
|
+
background: #FAFAFA;
|
|
163
|
+
border-radius: 16px;
|
|
164
|
+
padding: 20px;
|
|
165
|
+
margin: 30px 0;
|
|
166
|
+
border: 1px solid #eee;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.niki-progress-track {
|
|
170
|
+
background: #E0E0E0;
|
|
171
|
+
height: 10px;
|
|
172
|
+
border-radius: 10px;
|
|
173
|
+
width: 100%;
|
|
174
|
+
margin: 10px 0;
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.niki-progress-fill {
|
|
179
|
+
height: 100%;
|
|
180
|
+
background: linear-gradient(90deg, #FFAB91, #FF7043);
|
|
181
|
+
border-radius: 10px;
|
|
182
|
+
transition: width 1s ease;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Buton */
|
|
186
|
+
.niki-btn-action {
|
|
187
|
+
background: #4E342E;
|
|
188
|
+
color: white;
|
|
189
|
+
width: 100%;
|
|
190
|
+
padding: 16px;
|
|
191
|
+
border-radius: 14px;
|
|
192
|
+
font-size: 16px;
|
|
193
|
+
font-weight: 700;
|
|
194
|
+
border: none;
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
transition: transform 0.2s;
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
gap: 10px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.niki-btn-action:hover {
|
|
204
|
+
background: #3E2723;
|
|
205
|
+
transform: translateY(-2px);
|
|
206
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<form>
|
|
2
|
+
<div class="mb-3">
|
|
3
|
+
<label class="form-label" for="name">Name</label>
|
|
4
|
+
<input type="text" id="name" name="name" class="form-control" placeholder="Name" />
|
|
5
|
+
</div>
|
|
6
|
+
<div class="mb-3">
|
|
7
|
+
<label class="form-label" for="description">Description</label>
|
|
8
|
+
<input type="text" id="description" name="description" class="form-control" placeholder="Description" />
|
|
9
|
+
</div>
|
|
10
|
+
</form>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<li data-type="item" class="list-group-item">
|
|
2
|
+
<div class="d-flex gap-2 justify-content-between align-items-start">
|
|
3
|
+
<div class="flex-grow-1">
|
|
4
|
+
<strong>{name}</strong><br />
|
|
5
|
+
<small>{description}</small>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="d-flex gap-1 flex-nowrap">
|
|
8
|
+
<button type="button" data-type="edit" class="btn btn-sm btn-info">Edit</button>
|
|
9
|
+
<button type="button" data-type="remove" class="btn btn-sm btn-danger">Delete</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</li>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<div class="acp-page-container">
|
|
2
|
+
<!-- IMPORT admin/partials/settings/header.tpl -->
|
|
3
|
+
|
|
4
|
+
<div class="row m-0">
|
|
5
|
+
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
|
|
6
|
+
<form role="form" class="quickstart-settings">
|
|
7
|
+
<div class="mb-4">
|
|
8
|
+
<h5 class="fw-bold tracking-tight settings-header">General</h5>
|
|
9
|
+
|
|
10
|
+
<p class="lead">
|
|
11
|
+
Adjust these settings. You can then retrieve these settings in code via:
|
|
12
|
+
<br/><code>await meta.settings.get('quickstart');</code>
|
|
13
|
+
</p>
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label class="form-label" for="setting-1">Setting 1</label>
|
|
16
|
+
<input type="text" id="setting-1" name="setting-1" title="Setting 1" class="form-control" placeholder="Setting 1">
|
|
17
|
+
</div>
|
|
18
|
+
<div class="mb-3">
|
|
19
|
+
<label class="form-label" for="setting-2">Setting 2</label>
|
|
20
|
+
<input type="text" id="setting-2" name="setting-2" title="Setting 2" class="form-control" placeholder="Setting 2">
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="form-check form-switch">
|
|
24
|
+
<input type="checkbox" class="form-check-input" id="setting-3" name="setting-3">
|
|
25
|
+
<label for="setting-3" class="form-check-label">Setting 3</label>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="mb-4">
|
|
30
|
+
<h5 class="fw-bold tracking-tight settings-header">Colors</h5>
|
|
31
|
+
|
|
32
|
+
<p class="alert" id="preview">
|
|
33
|
+
Here is some preview text. Use the inputs below to modify this alert's appearance.
|
|
34
|
+
</p>
|
|
35
|
+
<div class="mb-3 d-flex gap-2">
|
|
36
|
+
<label class="form-label" for="color">Foreground</label>
|
|
37
|
+
<input data-settings="colorpicker" type="color" id="color" name="color" title="Background Color" class="form-control p-1" placeholder="#ffffff" value="#ffffff" style="width: 64px;"/>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="mb-3 d-flex gap-2">
|
|
40
|
+
<label class="form-label" for="bgColor">Background</label>
|
|
41
|
+
<input data-settings="colorpicker" type="color" id="bgColor" name="bgColor" title="Background Color" class="form-control p-1" placeholder="#000000" value="#000000" style="width: 64px;" />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="mb-4">
|
|
46
|
+
<h5 class="fw-bold tracking-tight settings-header">Sorted List</h5>
|
|
47
|
+
|
|
48
|
+
<div class="mb-3" data-type="sorted-list" data-sorted-list="sample-list" data-item-template="admin/plugins/quickstart/partials/sorted-list/item" data-form-template="admin/plugins/quickstart/partials/sorted-list/form">
|
|
49
|
+
<ul data-type="list" class="list-group mb-2"></ul>
|
|
50
|
+
<button type="button" data-type="add" class="btn btn-info">Add Item</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div>
|
|
55
|
+
<h5 class="fw-bold tracking-tight settings-header">Uploads</h5>
|
|
56
|
+
|
|
57
|
+
<label class="form-label" for="uploadedImage">Upload Image</label>
|
|
58
|
+
<div class="d-flex gap-1">
|
|
59
|
+
<input id="uploadedImage" name="uploadedImage" type="text" class="form-control" />
|
|
60
|
+
<input value="Upload" data-action="upload" data-target="uploadedImage" type="button" class="btn btn-light" />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</form>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- IMPORT admin/partials/settings/toc.tpl -->
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<div class="niki-wallet-wrapper">
|
|
2
|
+
<div class="niki-header-bg"></div>
|
|
3
|
+
|
|
4
|
+
<div class="niki-wallet-content">
|
|
5
|
+
<div class="niki-wallet-avatar">
|
|
6
|
+
<img src="https://i.imgur.com/kXUe4M6.png" alt="Niki">
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="niki-balance-label">Toplam Bakiye</div>
|
|
10
|
+
<div class="niki-balance-big">{points}</div>
|
|
11
|
+
|
|
12
|
+
<div class="niki-daily-stats">
|
|
13
|
+
<div style="display:flex; justify-content:space-between; font-size:12px; color:#888; font-weight:600;">
|
|
14
|
+
<span>Günlük Kazanım</span>
|
|
15
|
+
<span>{dailyScore} / {dailyCap}</span>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="niki-progress-track">
|
|
19
|
+
<div class="niki-progress-fill" style="width: {dailyPercent}%;"></div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div style="font-size:11px; color:#aaa;">
|
|
23
|
+
Bugün daha fazla çalışarak limitini doldurabilirsin!
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<button class="niki-btn-action">
|
|
28
|
+
<i class="fa fa-qrcode"></i> KAHVE AL (QR OLUŞTUR)
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
<p style="font-size:12px; color:#ccc; margin-top:15px;">
|
|
32
|
+
Niki The Cat Coffee © Loyalty Program
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
package/test/.eslintrc
ADDED
package/test/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* You can run these tests by executing `npx mocha test/plugins-installed.js`
|
|
3
|
+
* from the NodeBB root folder. The regular test runner will also run these
|
|
4
|
+
* tests.
|
|
5
|
+
*
|
|
6
|
+
* Keep in mind tests do not activate all plugins, so if you are testing
|
|
7
|
+
* hook listeners, socket.io, or mounted routes, you will need to add your
|
|
8
|
+
* plugin to `config.json`, e.g.
|
|
9
|
+
*
|
|
10
|
+
* {
|
|
11
|
+
* "test_plugins": [
|
|
12
|
+
* "nodebb-plugin-quickstart"
|
|
13
|
+
* ]
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
/* globals describe, it, before */
|
|
20
|
+
|
|
21
|
+
const assert = require('assert');
|
|
22
|
+
|
|
23
|
+
const db = require.main.require('./test/mocks/databasemock');
|
|
24
|
+
|
|
25
|
+
describe('nodebb-plugin-quickstart', () => {
|
|
26
|
+
before(() => {
|
|
27
|
+
// Prepare for tests here
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should pass', (done) => {
|
|
31
|
+
const actual = 'value';
|
|
32
|
+
const expected = 'value';
|
|
33
|
+
assert.strictEqual(actual, expected);
|
|
34
|
+
done();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should load config object', async () => { // Tests can be async functions too
|
|
38
|
+
const config = await db.getObject('config');
|
|
39
|
+
assert(config);
|
|
40
|
+
});
|
|
41
|
+
});
|