├── .editorconfig ├── .github └── issue_template.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── extend.php ├── js ├── admin.js ├── dist │ ├── admin.js │ ├── admin.js.map │ ├── forum.js │ └── forum.js.map ├── forum.js ├── package.json ├── src │ ├── admin │ │ ├── addSettingsPage.js │ │ ├── components │ │ │ └── SettingsPage.js │ │ └── index.js │ ├── common │ │ ├── helpers │ │ │ └── rankLabel.js │ │ ├── index.js │ │ └── models │ │ │ └── Rank.js │ └── forum │ │ ├── components │ │ ├── AddAttributes.js │ │ ├── AddHotnessSort.js │ │ ├── AddVoteButtons.js │ │ ├── RankingsPage.js │ │ ├── VoteNotification.js │ │ └── VotesModal.js │ │ └── index.js └── webpack.config.js ├── migrations ├── 2017_04_09_224815_create_posts_votes_table.php ├── 2017_04_09_225024_add_votes_to_users.php ├── 2017_04_24_094425_add_hotness_to_discussions.php ├── 2017_04_25__133721_add_default_vote_permissions.php ├── 2017_04_26_202436_create_users_ranks_table.php ├── 2017_04_26_202644_create_ranks_table.php ├── 2017_08_11_225322_add_default_ranking_permission.php ├── 2017_09_05_214452_add_time_attribute_to_users.php ├── 2018_08_02_110300_rename_users_ranks_to_rank_user.php └── 2018_08_02_110400_rename_posts_votes_to_post_votes.php ├── resources ├── less │ ├── admin │ │ └── extension.less │ ├── forum │ │ └── extension.less │ └── lib │ │ └── rankLabel.less └── locale │ ├── en.yml │ ├── es.yml │ ├── fr.yml │ ├── it.yml │ ├── pl.yml │ └── pt-br.yml ├── scripts ├── compile.sh └── push.sh └── src ├── Access └── DiscussionPolicy.php ├── Api ├── Controllers │ ├── ConvertLikesController.php │ ├── CreateRankController.php │ ├── DeleteRankController.php │ ├── DeleteTopImageController.php │ ├── ListRanksController.php │ ├── OrderByPointsController.php │ ├── UpdateRankController.php │ └── UploadTopImageController.php └── Serializers │ └── RankSerializer.php ├── Commands ├── CreateRank.php ├── CreateRankHandler.php ├── DeleteRank.php ├── DeleteRankHandler.php ├── EditRank.php └── EditRankHandler.php ├── Events └── PostWasVoted.php ├── Gambit └── HotGambit.php ├── Gamification.php ├── Likes.php ├── Listeners ├── AddRelationships.php ├── EventHandlers.php ├── FilterDiscussionListByHotness.php └── SaveVotesToDatabase.php ├── Notification └── VoteBlueprint.php ├── Rank.php ├── Validator └── RankValidator.php └── Vote.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | end_of_line = lf 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | [*.md] 10 | indent_size = 2 11 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Instructions (MUST READ FIRST) 2 | 3 | Before we can address your issues and concerns with the behavior of our work, we require every bit of detail from you to the fullest extent of this form. If this form is not properly filled out to it's entirity, we, ReFlar, reserve the right to close your issue due to lack of information thereof as well as not following instructions. 4 | 5 | * [x] By agreeing to these terms, you can simply leave this checkbox checked. 6 | 7 | ### Please Mentally Go Over the Following Items Before Filling Out this Form: 8 | 9 | * Can you reproduce this problem in debug mode? 10 | * Are you running the latest version of this extension? 11 | * Is your Flarum up-to-date? 12 | 13 | ### Description 14 | 15 | [Description of the bug or feature] 16 | 17 | ### Steps to Reproduce 18 | 19 | 1. [First Step] 20 | 2. [Second Step] 21 | 3. [and so on...] 22 | 23 | **Expected Behavior:** [What you expected to happen] 24 | 25 | **Actual Behavior:** [What actually happened (screenshots are welcomed!)] 26 | 27 | ### Flarum/Server/Client Information 28 | 29 | ## Technical Details 30 | 31 | - Version of Flarum (x.y.z): 32 | - Version of extension (x.y.z): 33 | - [OPTIONAL] Website URL where the bug is visible (https://example.com): 34 | - The webserver you are running (Apache, Nginx, etc): 35 | - PHP version (x.y.z): 36 | - Hosted environment (Shared, VPS, etc): 37 | - Hosting provider (https://some-amazing-provider.com): 38 | 39 | ## Flarum Info 40 | 41 | ``` 42 | Output of "php flarum info", run this in terminal in your Flarum directory. 43 | ``` 44 | 45 | ## Log Files 46 | 47 | ``` 48 | Put any relevant logs here. 49 | ``` 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | .DS_Store 4 | Thumbs.db 5 | node_modules 6 | bower_components 7 | .idea 8 | .floo* 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0-beta.1 2 | * Inital release 3 | 4 | # 0.1.0-beta.2 5 | * Fix bug for upvoting posts that aren't the OP 6 | 7 | # 0.1.0-beta.3 8 | #### After updating, please either disable and re-enable the extension or run `php flarum migrate` in your root dir 9 | * Added vote permission 10 | * Added floodgate protection 11 | 12 | # 0.1.0-beta3.1 13 | * Tiny fixes 14 | 15 | # 0.1.0-beta.4 16 | * Spanish locale, thank to the man himself, @AngelAvila 17 | 18 | # 0.1.0-beta.5 19 | * Fixes a major bug that converted likes would all be owned by the same user, reported by @AngelAvila 20 | * Added the ability for people to see who upvoted/downvoted a post configurable via permission 21 | 22 | # 0.1.0-beta.6 23 | * Fixes a bug that prevented the hot filter from working, found by @jordanjay29 24 | * Fixes a bug that prevented likes from converting, found by @AngelAvila 25 | * Fixes a bug that would show a white box around the icons with forums that had non-white backgrounds, found by @SierraKiloGulf 26 | * Added the ability to change the upvote and downvote icons 27 | * Added the ability to change the wording before the rank name on the profile page 28 | 29 | # 0.1.0-beta.6.2 30 | * Made the points transparent 31 | * Check added to make sure that the button will always have an active color even if the forum's colors are the same 32 | * Fixes a few issues introduced in the last release 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ReFlar 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gamification by ReFlar 2 | 3 | [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/reflar/gamification/blob/master/LICENSE.md) [![Latest Stable Version](https://img.shields.io/packagist/v/reflar/gamification.svg)](https://packagist.org/packages/reflar/gamification) [![Total Downloads](https://img.shields.io/packagist/dt/reflar/gamification.svg)](https://packagist.org/packages/reflar/gamification) 4 | 5 | A [Flarum](http://flarum.org) extension that adds upvotes, downvotes, and ranks to your Flarum Community! 6 | 7 | Upvote and downvote posts anonymously, and reward active users with ranks, and sort posts by hotness/popularity. 8 | 9 | ### Usage 10 | 11 | - Just click upvote or downvote 12 | - Posts can be sorted by "Hotness" 13 | 14 | ### Installation 15 | 16 | Install it with composer: 17 | 18 | ```bash 19 | composer require reflar/gamification 20 | ``` 21 | 22 | Then login and enable the extension. 23 | 24 | You can optionally convert your likes into upvotes, as well as calculate the hotness of all previous discussions. 25 | 26 | ### How hotness is sorted? 27 | 28 | The total amount of hotness is got between the amount of votes on the discussion and the posts inside of it. Also, newer posts with the same amount of upvotes as another post will have more hotness, so time is also an influent factor. 29 | 30 | ### Developer Guide 31 | 32 | You have 1 event to listen for `PostWasVoted` it contains the post, post's user, the actor, and the vote type (up or down). 33 | 34 | ### To Do 35 | 36 | - Requests? 37 | 38 | ### Important 39 | 40 | This Extension is meant as a replacement for the Flarum Likes Extension. Therefore, they are not compatible and it's recommended to disable the Likes Extension. 41 | 42 | ### Issues 43 | 44 | - [Open an issue on GitHub](https://github.com/ReFlar/gamification/issues) 45 | 46 | ### Links 47 | 48 | - [on github](https://github.com/ReFlar/gamification) 49 | - [on packagist](https://packagist.org/packages/ReFlar/gamification) 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reflar/gamification", 3 | "description": "Upvotes and downvotes for your Flarum community", 4 | "keywords": [ 5 | "misc", 6 | "settings", 7 | "flarum", 8 | "reflar", 9 | "points", 10 | "gamification" 11 | ], 12 | "type": "flarum-extension", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Christian Lopez", 17 | "email": "Christian.Lopez@redevs.org", 18 | "homepage": "https://redevs.org" 19 | }, 20 | 21 | { 22 | "name": "Charlie K", 23 | "email": "Charlie.K@redevs.org", 24 | "homepage": "https://redevs.org" 25 | } 26 | ], 27 | "support": { 28 | "issues": "https://github.com/ReDevelopers/ReFlar/gamification/issues", 29 | "source": "https://github.com/ReDevelopers/ReFlar/gamification" 30 | }, 31 | "require": { 32 | "flarum/core": "^0.1.0-beta.8" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Reflar\\Gamification\\": "src/" 37 | } 38 | }, 39 | "extra": { 40 | "flarum-extension": { 41 | "title": "ReFlar Gamification", 42 | "icon": { 43 | "name": "fa fa-thumbs-up", 44 | "backgroundColor": "#263238", 45 | "color": "#fff" 46 | } 47 | }, 48 | "flagrow": { 49 | "discuss": "https://discuss.flarum.org/d/5588-gamification-by-reflar" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /extend.php: -------------------------------------------------------------------------------- 1 | css(__DIR__ . '/resources/less/admin/extension.less') 22 | ->js(__DIR__.'/js/dist/admin.js'), 23 | (new Extend\Frontend('forum')) 24 | ->js(__DIR__.'/js/dist/forum.js') 25 | ->css(__DIR__ . '/resources/less/forum/extension.less') 26 | ->route('/rankings', 'rankings') 27 | ->route('/hot', 'hot'), 28 | new Extend\Locales(__DIR__ . '/resources/locale'), 29 | (new Extend\Routes('api')) 30 | ->post('/reflar/gamification/convert', 'reflar.gamification.convert', Controllers\ConvertLikesController::class) 31 | ->get('/ranks', 'ranks.index', Controllers\ListRanksController::class) 32 | ->post('/ranks', 'ranks.create', Controllers\CreateRankController::class) 33 | ->post('/topimage{id}', 'reflar.topImage.add', Controllers\UploadTopImageController::class) 34 | ->delete('/topimage{id}', 'reflar.topImage.delete', Controllers\DeleteTopImageController::class) 35 | ->patch('/ranks/{id}', 'ranks.update', Controllers\UpdateRankController::class) 36 | ->delete('/ranks/{id}', 'ranks.delete', Controllers\DeleteRankController::class) 37 | ->get('/rankings', 'rankings', Controllers\OrderByPointsController::class), 38 | function (Dispatcher $events) { 39 | $events->subscribe(Listeners\AddRelationships::class); 40 | $events->subscribe(Listeners\EventHandlers::class); 41 | $events->subscribe(Listeners\SaveVotesToDatabase::class); 42 | $events->subscribe(Listeners\FilterDiscussionListByHotness::class); 43 | 44 | $events->subscribe(Access\DiscussionPolicy::class); 45 | }, 46 | ]; 47 | -------------------------------------------------------------------------------- /js/admin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Flarum. 3 | * 4 | * (c) Toby Zerner 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export * from './src/common'; 11 | export * from './src/admin'; -------------------------------------------------------------------------------- /js/dist/admin.js: -------------------------------------------------------------------------------- 1 | module.exports=function(a){var t={};function n(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return a[e].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=a,n.c=t,n.d=function(a,t,e){n.o(a,t)||Object.defineProperty(a,t,{enumerable:!0,get:e})},n.r=function(a){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(a,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(a,"__esModule",{value:!0})},n.t=function(a,t){if(1&t&&(a=n(a)),8&t)return a;if(4&t&&"object"==typeof a&&a&&a.__esModule)return a;var e=Object.create(null);if(n.r(e),Object.defineProperty(e,"default",{enumerable:!0,value:a}),2&t&&"string"!=typeof a)for(var r in a)n.d(e,r,function(t){return a[t]}.bind(null,r));return e},n.n=function(a){var t=a&&a.__esModule?function(){return a.default}:function(){return a};return n.d(t,"a",t),t},n.o=function(a,t){return Object.prototype.hasOwnProperty.call(a,t)},n.p="",n(n.s=38)}([function(a,t){a.exports=flarum.core.compat.app},function(a,t){a.exports=flarum.core.compat.extend},function(a,t){a.exports=flarum.core.compat.Model},function(a,t){a.exports=flarum.core.compat["components/Button"]},function(a,t,n){"use strict";function e(a,t){a.prototype=Object.create(t.prototype),a.prototype.constructor=a,a.__proto__=t}n.d(t,"a",function(){return e})},,function(a,t){},function(a,t,n){"use strict";n.d(t,"a",function(){return s});var e=n(4),r=n(2),i=n.n(r),o=n(8),s=function(a){function t(){return a.apply(this,arguments)||this}return Object(e.a)(t,a),t}(n.n(o)()(i.a,{points:i.a.attribute("points"),name:i.a.attribute("name"),color:i.a.attribute("color")}))},function(a,t){a.exports=flarum.core.compat["utils/mixin"]},function(a,t){a.exports=flarum.core.compat["components/Page"]},,,,,,function(a,t){a.exports=flarum.core.compat["components/UploadImageButton"]},,,,,,function(a,t){a.exports=flarum.core.compat["components/PermissionGrid"]},function(a,t){a.exports=flarum.core.compat["components/Switch"]},,function(a,t,n){"use strict";var e=n(0),r=n.n(e),i=n(1),o=n(21),s=n.n(o),l=n(33),c=n.n(l),p=n(34),u=n.n(p),f=n(4),d=n(35),g=n.n(d),h=n(9),v=n.n(h),k=n(3),b=n.n(k),w=n(15),x=n.n(w),R=n(36),N=n.n(R),y=n(22),P=n.n(y),_=function(a){function t(){return a.apply(this,arguments)||this}Object(f.a)(t,a);var n=t.prototype;return n.init=function(){var a=this;this.fields=["convertedLikes","amountPerPost","amountPerDiscussion","postStartAmount","rankAmt","iconName","blockedUsers","pointsPlaceholder"],this.switches=["autoUpvotePosts","customRankingImages"],this.ranks=app.store.all("ranks"),this.values={},this.settingsPrefix="reflar.gamification";var t=app.data.settings;this.fields.forEach(function(n){return a.values[n]=m.prop(t[a.addPrefix(n)])}),this.switches.forEach(function(n){return a.values[n]=m.prop("1"===t[a.addPrefix(n)])}),this.newRank={points:m.prop(""),name:m.prop(""),color:m.prop("")}},n.view=function(){var a=this;return[m("div",{className:"SettingsPage"},[m("div",{className:"container"},[m("form",{onsubmit:this.onsubmit.bind(this)},[m("div",{className:"helpText"},app.translator.trans("reflar-gamification.admin.page.convert.help")),void 0===this.values.convertedLikes()?b.a.component({type:"button",className:"Button Button--warning Ranks-button",children:app.translator.trans("reflar-gamification.admin.page.convert.button"),onclick:function(){app.request({url:app.forum.attribute("apiUrl")+"/reflar/gamification/convert",method:"POST"}).then(a.values.convertedLikes("converting"))}}):"converting"===this.values.convertedLikes()?m("label",{},app.translator.trans("reflar-gamification.admin.page.convert.converting")):m("label",{},app.translator.trans("reflar-gamification.admin.page.convert.converted",{number:this.values.convertedLikes()})),m("fieldset",{className:"SettingsPage-ranks"},[m("legend",{},app.translator.trans("reflar-gamification.admin.page.ranks.title")),m("label",{},app.translator.trans("reflar-gamification.admin.page.ranks.ranks")),m("div",{className:"helpText"},app.translator.trans("reflar-gamification.admin.page.ranks.help.help")),m("div",{className:"Ranks--Container"},this.ranks.map(function(t){return m("div",{style:"float: left;"},[m("input",{className:"FormControl Ranks-number",type:"number",value:t.points(),placeholder:app.translator.trans("reflar-gamification.admin.page.ranks.help.points"),oninput:m.withAttr("value",a.updatePoints.bind(a,t))}),m("input",{className:"FormControl Ranks-name",value:t.name(),placeholder:app.translator.trans("reflar-gamification.admin.page.ranks.help.name"),oninput:m.withAttr("value",a.updateName.bind(a,t))}),m("input",{className:"FormControl Ranks-color",value:t.color(),placeholder:app.translator.trans("reflar-gamification.admin.page.ranks.help.color"),oninput:m.withAttr("value",a.updateColor.bind(a,t))}),b.a.component({type:"button",className:"Button Button--warning Ranks-button",icon:"fa fa-times",onclick:a.deleteRank.bind(a,t)})])}),m("div",{style:"float: left; margin-bottom: 15px"},[m("input",{className:"FormControl Ranks-number",value:this.newRank.points(),placeholder:app.translator.trans("reflar-gamification.admin.page.ranks.help.points"),type:"number",oninput:m.withAttr("value",this.newRank.points)}),m("input",{className:"FormControl Ranks-name",value:this.newRank.name(),placeholder:app.translator.trans("reflar-gamification.admin.page.ranks.help.name"),oninput:m.withAttr("value",this.newRank.name)}),m("input",{className:"FormControl Ranks-color",value:this.newRank.color(),placeholder:app.translator.trans("reflar-gamification.admin.page.ranks.help.color"),oninput:m.withAttr("value",this.newRank.color)}),b.a.component({type:"button",className:"Button Button--warning Ranks-button",icon:"fa fa-plus",onclick:this.addRank.bind(this)})])),m("label",{},app.translator.trans("reflar-gamification.admin.page.ranks.number_title")),m("input",{className:"FormControl Ranks-default",value:this.values.rankAmt()||"",placeholder:2,oninput:m.withAttr("value",this.values.rankAmt)}),m("legend",{},app.translator.trans("reflar-gamification.admin.page.votes.title")),m("label",{},app.translator.trans("reflar-gamification.admin.page.votes.icon_name")),m("div",{className:"helpText"},app.translator.trans("reflar-gamification.admin.page.votes.icon_help")),m("input",{className:"FormControl Ranks-default",value:this.values.iconName()||"",placeholder:"thumbs",oninput:m.withAttr("value",this.values.iconName)}),P.a.component({state:this.values.autoUpvotePosts()||!1,children:app.translator.trans("reflar-gamification.admin.page.votes.auto_upvote"),onchange:this.values.autoUpvotePosts,className:"votes-switch"}),m("label",{},app.translator.trans("reflar-gamification.admin.page.votes.points_title")),m("input",{className:"FormControl Ranks-default",value:this.values.pointsPlaceholder()||"",placeholder:app.translator.trans("reflar-gamification.admin.page.votes.points_placeholder")+"{points}",oninput:m.withAttr("value",this.values.pointsPlaceholder)}),m("legend",{},app.translator.trans("reflar-gamification.admin.page.rankings.title")),P.a.component({state:this.values.customRankingImages()||!1,children:app.translator.trans("reflar-gamification.admin.page.rankings.enable"),onchange:this.values.customRankingImages,className:"votes-switch"}),m("label",{},app.translator.trans("reflar-gamification.admin.page.rankings.blocked.title")),m("input",{className:"FormControl Ranks-blocked",placeholder:app.translator.trans("reflar-gamification.admin.page.rankings.blocked.placeholder"),value:this.values.blockedUsers()||"",oninput:m.withAttr("value",this.values.blockedUsers)}),m("div",{className:"helpText"},app.translator.trans("reflar-gamification.admin.page.rankings.blocked.help")),m("label",{className:"Upload-label"},app.translator.trans("reflar-gamification.admin.page.rankings.custom_image_1")),m(x.a,{className:"Upload-button",name:"topimage1"}),m("br"),m("label",{className:"Upload-label"},app.translator.trans("reflar-gamification.admin.page.rankings.custom_image_2")),m(x.a,{className:"Upload-button",name:"topimage2"}),m("br"),m("label",{className:"Upload-label"},app.translator.trans("reflar-gamification.admin.page.rankings.custom_image_3")),m(x.a,{className:"Upload-button",name:"topimage3"}),m("br"),b.a.component({type:"submit",className:"Button Button--primary Ranks-save",children:app.translator.trans("reflar-gamification.admin.page.save_settings"),loading:this.loading,disabled:!this.changed()})])])])])]},n.updateName=function(a,t){a.save({name:t})},n.updatePoints=function(a,t){a.save({points:t})},n.updateColor=function(a,t){a.save({color:t})},n.deleteRank=function(a){var t=this;a.delete(),this.ranks.some(function(n,e){if(n.data.id===a.data.id)return t.ranks.splice(e,1),!0})},n.addRank=function(a){var t=this;app.store.createRecord("ranks").save({points:this.newRank.points(),name:this.newRank.name(),color:this.newRank.color()}).then(function(a){t.newRank.color(""),t.newRank.name(""),t.newRank.points(""),t.ranks.push(a),m.redraw()})},n.changed=function(){var a=this,t=this.switches.some(function(t){return a.values[t]()!==("1"==app.data.settings[a.addPrefix(t)])});return this.fields.some(function(t){return a.values[t]()!==app.data.settings[a.addPrefix(t)]})||t},n.onsubmit=function(a){var t=this;if(a.preventDefault(),!this.loading){this.loading=!0,app.alerts.dismiss(this.successAlert);var n={};this.switches.forEach(function(a){return n[t.addPrefix(a)]=t.values[a]()}),this.fields.forEach(function(a){return n[t.addPrefix(a)]=t.values[a]()}),N()(n).then(function(){app.alerts.show(t.successAlert=new g.a({type:"success",children:app.translator.trans("core.admin.basics.saved_message")}))}).catch(function(){}).then(function(){t.loading=!1,window.location.reload()})}},n.addPrefix=function(a){return this.settingsPrefix+"."+a},t}(v.a),A=function(){app.routes["reflar-gamification"]={path:"/reflar/gamification",component:_.component()},app.extensionSettings["reflar-gamification"]=function(){return m.route(app.route("reflar-gamification"))},Object(i.extend)(c.a.prototype,"items",function(a){a.add("reflar-gamification",u.a.component({href:app.route("reflar-gamification"),icon:"fas fa-thumbs-up",children:"Gamification",description:app.translator.trans("reflar-gamification.admin.nav.desc")}))})},O=n(7);r.a.initializers.add("reflar-gamification",function(a){a.store.models.ranks=O.a,Object(i.extend)(s.a.prototype,"replyItems",function(t){t.add("Vote",{icon:"fas fa-thumbs-up",label:a.translator.trans("reflar-gamification.admin.permissions.vote_label"),permission:"discussion.vote"})}),Object(i.extend)(s.a.prototype,"viewItems",function(t){t.add("canSeeVotes",{icon:"fas fa-info-circle",label:a.translator.trans("reflar-gamification.admin.permissions.see_votes_label"),permission:"discussion.canSeeVotes"}),t.add("canViewRankingPage",{icon:"fas fa-trophy",label:a.translator.trans("reflar-gamification.admin.permissions.see_ranking_page"),permission:"reflar.gamification.viewRankingPage",allowGuest:!0})}),A()})},,,,,,,,,function(a,t){a.exports=flarum.core.compat["components/AdminNav"]},function(a,t){a.exports=flarum.core.compat["components/AdminLinkButton"]},function(a,t){a.exports=flarum.core.compat["components/Alert"]},function(a,t){a.exports=flarum.core.compat["utils/saveSettings"]},,function(a,t,n){"use strict";n.r(t);var e=n(6);for(var r in e)"default"!==r&&function(a){n.d(t,a,function(){return e[a]})}(r);n(24)}]); 2 | //# sourceMappingURL=admin.js.map -------------------------------------------------------------------------------- /js/dist/admin.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://@reflar/gamification/webpack/bootstrap","webpack://@reflar/gamification/external \"flarum.core.compat['app']\"","webpack://@reflar/gamification/external \"flarum.core.compat['extend']\"","webpack://@reflar/gamification/external \"flarum.core.compat['Model']\"","webpack://@reflar/gamification/external \"flarum.core.compat['components/Button']\"","webpack://@reflar/gamification/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js","webpack://@reflar/gamification/./src/common/models/Rank.js","webpack://@reflar/gamification/external \"flarum.core.compat['utils/mixin']\"","webpack://@reflar/gamification/external \"flarum.core.compat['components/Page']\"","webpack://@reflar/gamification/external \"flarum.core.compat['components/UploadImageButton']\"","webpack://@reflar/gamification/external \"flarum.core.compat['components/PermissionGrid']\"","webpack://@reflar/gamification/external \"flarum.core.compat['components/Switch']\"","webpack://@reflar/gamification/./src/admin/components/SettingsPage.js","webpack://@reflar/gamification/./src/admin/addSettingsPage.js","webpack://@reflar/gamification/./src/admin/index.js","webpack://@reflar/gamification/external \"flarum.core.compat['components/AdminNav']\"","webpack://@reflar/gamification/external \"flarum.core.compat['components/AdminLinkButton']\"","webpack://@reflar/gamification/external \"flarum.core.compat['components/Alert']\"","webpack://@reflar/gamification/external \"flarum.core.compat['utils/saveSettings']\"","webpack://@reflar/gamification/./admin.js"],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","flarum","core","compat","_inheritsLoose","subClass","superClass","constructor","__proto__","__webpack_exports__","Rank","mixin","Model","points","attribute","color","SettingsPage","init","_this","this","fields","switches","ranks","app","store","all","values","settingsPrefix","settings","data","forEach","prop","addPrefix","newRank","view","_this2","className","onsubmit","translator","trans","undefined","convertedLikes","Button","component","type","children","onclick","request","url","forum","method","then","number","map","rank","style","placeholder","oninput","withAttr","updatePoints","updateName","updateColor","icon","deleteRank","addRank","rankAmt","iconName","Switch","state","autoUpvotePosts","onchange","pointsPlaceholder","customRankingImages","blockedUsers","UploadImageButton_default","a","loading","disabled","changed","save","rankToDelete","_this3","delete","some","id","splice","_this4","createRecord","push","redraw","_this5","switchesCheck","e","_this6","preventDefault","alerts","dismiss","successAlert","saveSettings","show","Alert","catch","window","location","reload","Page","addSettingsPage","routes","path","extensionSettings","route","extend","AdminNav","items","add","AdminLinkButton","href","description","initializers","models","PermissionGrid","label","permission","allowGuest","_src_common__WEBPACK_IMPORTED_MODULE_0__","__WEBPACK_IMPORT_KEY__"],"mappings":"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","file":"admin.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 38);\n","module.exports = flarum.core.compat['app'];","module.exports = flarum.core.compat['extend'];","module.exports = flarum.core.compat['Model'];","module.exports = flarum.core.compat['components/Button'];","export default function _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n subClass.__proto__ = superClass;\n}","import Model from 'flarum/Model';\r\nimport mixin from 'flarum/utils/mixin';\r\n\r\nexport default class Rank extends mixin(Model, {\r\n points: Model.attribute('points'),\r\n name: Model.attribute('name'),\r\n color: Model.attribute('color')\r\n}) {}","module.exports = flarum.core.compat['utils/mixin'];","module.exports = flarum.core.compat['components/Page'];","module.exports = flarum.core.compat['components/UploadImageButton'];","module.exports = flarum.core.compat['components/PermissionGrid'];","module.exports = flarum.core.compat['components/Switch'];","import Alert from \"flarum/components/Alert\";\nimport Page from 'flarum/components/Page';\nimport Button from \"flarum/components/Button\";\nimport UploadImageButton from 'flarum/components/UploadImageButton';\nimport saveSettings from \"flarum/utils/saveSettings\";\nimport Switch from \"flarum/components/Switch\";\n\nexport default class SettingsPage extends Page {\n\n init() {\n\n this.fields = [\n 'convertedLikes',\n 'amountPerPost',\n 'amountPerDiscussion',\n 'postStartAmount',\n 'rankAmt',\n 'iconName',\n 'blockedUsers',\n 'pointsPlaceholder'\n ];\n\n this.switches = [\n 'autoUpvotePosts',\n 'customRankingImages'\n ];\n\n this.ranks = app.store.all('ranks');\n\n this.values = {};\n\n this.settingsPrefix = 'reflar.gamification';\n\n const settings = app.data.settings;\n\n this.fields.forEach(key =>\n this.values[key] = m.prop(settings[this.addPrefix(key)])\n );\n\n this.switches.forEach(key =>\n this.values[key] = m.prop(settings[this.addPrefix(key)] === '1')\n );\n\n this.newRank = {\n 'points': m.prop(''),\n 'name': m.prop(''),\n 'color': m.prop('')\n };\n }\n\n /**\n * @returns {*}\n */\n view() {\n return [\n m('div', {className: 'SettingsPage'}, [\n m('div', {className: 'container'}, [\n m('form', {onsubmit: this.onsubmit.bind(this)}, [\n m('div', {className: 'helpText'}, app.translator.trans('reflar-gamification.admin.page.convert.help')),\n (this.values.convertedLikes() === undefined ? (\n Button.component({\n type: 'button',\n className: 'Button Button--warning Ranks-button',\n children: app.translator.trans('reflar-gamification.admin.page.convert.button'),\n onclick: () => {\n app.request({\n url: app.forum.attribute('apiUrl') + '/reflar/gamification/convert',\n method: 'POST'\n }).then(this.values.convertedLikes('converting'));\n }\n })\n ) : (this.values.convertedLikes() === 'converting' ? (\n m('label', {}, app.translator.trans('reflar-gamification.admin.page.convert.converting'))\n ) : (m('label', {}, app.translator.trans('reflar-gamification.admin.page.convert.converted', {number: this.values.convertedLikes()}))))),\n\n m('fieldset', {className: 'SettingsPage-ranks'}, [\n m('legend', {}, app.translator.trans('reflar-gamification.admin.page.ranks.title')),\n m('label', {}, app.translator.trans('reflar-gamification.admin.page.ranks.ranks')),\n m('div', {className: 'helpText'}, app.translator.trans('reflar-gamification.admin.page.ranks.help.help')),\n m('div', {className: 'Ranks--Container'},\n this.ranks.map(rank => {\n return m('div', {style: \"float: left;\"}, [\n m('input', {\n className: 'FormControl Ranks-number',\n type: 'number',\n value: rank.points(),\n placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.points'),\n oninput: m.withAttr('value', this.updatePoints.bind(this, rank))\n }),\n m('input', {\n className: 'FormControl Ranks-name',\n value: rank.name(),\n placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.name'),\n oninput: m.withAttr('value', this.updateName.bind(this, rank))\n }),\n m('input', {\n className: 'FormControl Ranks-color',\n value: rank.color(),\n placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.color'),\n oninput: m.withAttr('value', this.updateColor.bind(this, rank))\n }),\n Button.component({\n type: 'button',\n className: 'Button Button--warning Ranks-button',\n icon: 'fa fa-times',\n onclick: this.deleteRank.bind(this, rank)\n }),\n ])\n }),\n m('div', {style: \"float: left; margin-bottom: 15px\"}, [\n m('input', {\n className: 'FormControl Ranks-number',\n value: this.newRank.points(),\n placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.points'),\n type: 'number',\n oninput: m.withAttr('value', this.newRank.points)\n }),\n m('input', {\n className: 'FormControl Ranks-name',\n value: this.newRank.name(),\n placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.name'),\n oninput: m.withAttr('value', this.newRank.name)\n }),\n m('input', {\n className: 'FormControl Ranks-color',\n value: this.newRank.color(),\n placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.color'),\n oninput: m.withAttr('value', this.newRank.color)\n }\n ),\n Button.component({\n type: 'button',\n className: 'Button Button--warning Ranks-button',\n icon: 'fa fa-plus',\n onclick: this.addRank.bind(this)\n }),\n ])\n ),\n m('label', {}, app.translator.trans('reflar-gamification.admin.page.ranks.number_title')),\n m('input', {\n className: 'FormControl Ranks-default',\n value: this.values.rankAmt() || '',\n placeholder: 2,\n oninput: m.withAttr('value', this.values.rankAmt)\n }),\n m('legend', {}, app.translator.trans('reflar-gamification.admin.page.votes.title')),\n m('label', {}, app.translator.trans('reflar-gamification.admin.page.votes.icon_name')),\n m('div', {className: 'helpText'}, app.translator.trans('reflar-gamification.admin.page.votes.icon_help')),\n m('input', {\n className: 'FormControl Ranks-default',\n value: this.values.iconName() || '',\n placeholder: 'thumbs',\n oninput: m.withAttr('value', this.values.iconName)\n }),\n Switch.component({\n state: this.values.autoUpvotePosts() || false,\n children: app.translator.trans('reflar-gamification.admin.page.votes.auto_upvote'),\n onchange: this.values.autoUpvotePosts,\n className: 'votes-switch'\n }),\n m('label', {}, app.translator.trans('reflar-gamification.admin.page.votes.points_title')),\n m('input', {\n className: 'FormControl Ranks-default',\n value: this.values.pointsPlaceholder() || '',\n placeholder: app.translator.trans('reflar-gamification.admin.page.votes.points_placeholder') + '{points}',\n oninput: m.withAttr('value', this.values.pointsPlaceholder)\n }),\n m('legend', {}, app.translator.trans('reflar-gamification.admin.page.rankings.title')),\n Switch.component({\n state: this.values.customRankingImages() || false,\n children: app.translator.trans('reflar-gamification.admin.page.rankings.enable'),\n onchange: this.values.customRankingImages,\n className: 'votes-switch'\n }),\n m('label', {}, app.translator.trans('reflar-gamification.admin.page.rankings.blocked.title')),\n m('input', {\n className: 'FormControl Ranks-blocked',\n placeholder: app.translator.trans('reflar-gamification.admin.page.rankings.blocked.placeholder'),\n value: this.values.blockedUsers() || '',\n oninput: m.withAttr('value', this.values.blockedUsers)\n }),\n m('div', { className: 'helpText' }, app.translator.trans('reflar-gamification.admin.page.rankings.blocked.help')),\n m('label', { className: 'Upload-label' }, app.translator.trans('reflar-gamification.admin.page.rankings.custom_image_1')),\n ,\n m('br'),\n m('label', { className: 'Upload-label' }, app.translator.trans('reflar-gamification.admin.page.rankings.custom_image_2')),\n ,\n m('br'),\n m('label', { className: 'Upload-label' }, app.translator.trans('reflar-gamification.admin.page.rankings.custom_image_3')),\n ,\n m('br'),\n Button.component({\n type: 'submit',\n className: 'Button Button--primary Ranks-save',\n children: app.translator.trans('reflar-gamification.admin.page.save_settings'),\n loading: this.loading,\n disabled: !this.changed()\n })\n ])\n ])\n ])\n ])\n ];\n }\n\n\n updateName(rank, value) {\n rank.save({name: value});\n }\n\n updatePoints(rank, value) {\n rank.save({points: value});\n }\n\n updateColor(rank, value) {\n rank.save({color: value});\n }\n\n deleteRank(rankToDelete) {\n rankToDelete.delete();\n this.ranks.some((rank, i) => {\n if (rank.data.id === rankToDelete.data.id) {\n this.ranks.splice(i, 1);\n return true;\n }\n })\n }\n\n addRank(rank) {\n app.store.createRecord('ranks').save({\n points: this.newRank.points(),\n name: this.newRank.name(),\n color: this.newRank.color()\n }).then(\n rank => {\n this.newRank.color('');\n this.newRank.name('');\n this.newRank.points('');\n this.ranks.push(rank);\n m.redraw();\n }\n );\n }\n\n\n /**\n *\n * @returns boolean\n */\n changed() {\n var switchesCheck = this.switches.some(key => this.values[key]() !== (app.data.settings[this.addPrefix(key)] == '1'));\n var fieldsCheck = this.fields.some(key => this.values[key]() !== app.data.settings[this.addPrefix(key)]);\n return fieldsCheck || switchesCheck;\n }\n\n /**\n * @param e\n */\n onsubmit(e) {\n e.preventDefault();\n\n if (this.loading) return;\n\n this.loading = true;\n\n app.alerts.dismiss(this.successAlert);\n\n const settings = {};\n\n this.switches.forEach(key => settings[this.addPrefix(key)] = this.values[key]());\n this.fields.forEach(key => settings[this.addPrefix(key)] = this.values[key]());\n\n saveSettings(settings)\n .then(() => {\n app.alerts.show(this.successAlert = new Alert({\n type: 'success',\n children: app.translator.trans('core.admin.basics.saved_message')\n }));\n })\n .catch(() => {\n })\n .then(() => {\n this.loading = false;\n window.location.reload();\n });\n }\n\n /**\n * @returns string\n */\n addPrefix(key) {\n return this.settingsPrefix + '.' + key;\n }\n}\n","import {extend} from \"flarum/extend\";\r\nimport AdminNav from \"flarum/components/AdminNav\";\r\nimport AdminLinkButton from \"flarum/components/AdminLinkButton\";\r\nimport SettingsPage from \"./components/SettingsPage\";\r\n\r\nexport default function () {\r\n app.routes['reflar-gamification'] = {path: '/reflar/gamification', component: SettingsPage.component()};\r\n\r\n app.extensionSettings['reflar-gamification'] = () => m.route(app.route('reflar-gamification'));\r\n\r\n extend(AdminNav.prototype, 'items', items => {\r\n items.add('reflar-gamification', AdminLinkButton.component({\r\n href: app.route('reflar-gamification'),\r\n icon: 'fas fa-thumbs-up',\r\n children: 'Gamification',\r\n description: app.translator.trans('reflar-gamification.admin.nav.desc')\r\n }));\r\n });\r\n}\r\n","import app from 'flarum/app'\r\nimport {extend} from 'flarum/extend'\r\nimport PermissionGrid from 'flarum/components/PermissionGrid'\r\nimport addSettingsPage from './addSettingsPage'\r\nimport Rank from '../common/models/Rank'\r\n\r\napp.initializers.add('reflar-gamification', app => {\r\n app.store.models.ranks = Rank;\r\n\r\n extend(PermissionGrid.prototype, 'replyItems', items => {\r\n items.add('Vote', {\r\n icon: 'fas fa-thumbs-up',\r\n label: app.translator.trans('reflar-gamification.admin.permissions.vote_label'),\r\n permission: 'discussion.vote'\r\n })\r\n });\r\n\r\n extend(PermissionGrid.prototype, 'viewItems', items => {\r\n items.add('canSeeVotes', {\r\n icon: 'fas fa-info-circle',\r\n label: app.translator.trans('reflar-gamification.admin.permissions.see_votes_label'),\r\n permission: 'discussion.canSeeVotes'\r\n });\r\n items.add('canViewRankingPage', {\r\n icon: 'fas fa-trophy',\r\n label: app.translator.trans('reflar-gamification.admin.permissions.see_ranking_page'),\r\n permission: 'reflar.gamification.viewRankingPage',\r\n allowGuest: true\r\n })\r\n });\r\n\r\n addSettingsPage();\r\n});\r\n","module.exports = flarum.core.compat['components/AdminNav'];","module.exports = flarum.core.compat['components/AdminLinkButton'];","module.exports = flarum.core.compat['components/Alert'];","module.exports = flarum.core.compat['utils/saveSettings'];","/*\r\n * This file is part of Flarum.\r\n *\r\n * (c) Toby Zerner \r\n *\r\n * For the full copyright and license information, please view the LICENSE\r\n * file that was distributed with this source code.\r\n */\r\n\r\nexport * from './src/common';\r\nexport * from './src/admin';"],"sourceRoot":""} -------------------------------------------------------------------------------- /js/dist/forum.js: -------------------------------------------------------------------------------- 1 | module.exports=function(t){var n={};function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=t,e.c=n,e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:r})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,n){if(1&n&&(t=e(t)),8&n)return t;if(4&n&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(e.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&n&&"string"!=typeof t)for(var o in t)e.d(r,o,function(n){return t[n]}.bind(null,o));return r},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},e.p="",e(e.s=37)}([function(t,n){t.exports=flarum.core.compat.app},function(t,n){t.exports=flarum.core.compat.extend},function(t,n){t.exports=flarum.core.compat.Model},function(t,n){t.exports=flarum.core.compat["components/Button"]},function(t,n,e){"use strict";function r(t,n){t.prototype=Object.create(n.prototype),t.prototype.constructor=t,t.__proto__=n}e.d(n,"a",function(){return r})},function(t,n){t.exports=flarum.core.compat["components/IndexPage"]},function(t,n){},function(t,n,e){"use strict";e.d(n,"a",function(){return i});var r=e(4),o=e(2),s=e.n(o),a=e(8),i=function(t){function n(){return t.apply(this,arguments)||this}return Object(r.a)(n,t),n}(e.n(a)()(s.a,{points:s.a.attribute("points"),name:s.a.attribute("name"),color:s.a.attribute("color")}))},function(t,n){t.exports=flarum.core.compat["utils/mixin"]},function(t,n){t.exports=flarum.core.compat["components/Page"]},function(t,n){t.exports=flarum.core.compat["components/LogInModal"]},function(t,n){t.exports=flarum.core.compat["helpers/avatar"]},function(t,n){t.exports=flarum.core.compat["helpers/username"]},function(t,n){t.exports=flarum.core.compat["utils/ItemList"]},function(t,n){t.exports=flarum.core.compat["models/Discussion"]},,function(t,n){t.exports=flarum.core.compat["models/Post"]},function(t,n){t.exports=flarum.core.compat["models/User"]},function(t,n){t.exports=flarum.core.compat["components/UserCard"]},function(t,n){t.exports=flarum.core.compat["components/DiscussionList"]},function(t,n){t.exports=flarum.core.compat["components/CommentPost"]},,,function(t,n,e){"use strict";var r=e(1),o=e(0),s=e.n(o),a=e(14),i=e.n(a),u=e(2),c=e.n(u),p=e(16),f=e.n(p),l=e(25),d=e.n(l),h=e(17),b=e.n(h),g=e(18),v=e.n(g);function y(t,n){void 0===n&&(n={}),n.style=n.style||{},n.className="rankLabel "+(n.className||"");var e=t.color();return n.style.backgroundColor=n.style.color=e,n.className+=" colored",m("span",n,m("span",{className:"rankLabel-text"},t.name()))}var k=function(){i.a.prototype.canVote=c.a.attribute("canVote"),i.a.prototype.canSeeVotes=c.a.attribute("canSeeVotes"),i.a.prototype.votes=c.a.attribute("votes"),b.a.prototype.points=c.a.attribute("points"),b.a.prototype.ranks=c.a.hasMany("ranks"),f.a.prototype.upvotes=c.a.hasMany("upvotes"),f.a.prototype.downvotes=c.a.hasMany("downvotes");var t=function(t){return function(n){return n&&n.attrs&&n.attrs.className&&n.attrs.className===t}};Object(r.extend)(v.a.prototype,"infoItems",function(t,n){var e="";0==e&&(e="0"),e=app.forum.attribute("PointsPlaceholder")?app.forum.attribute("PointsPlaceholder").replace("{points}",this.props.user.data.attributes.Points):app.translator.trans("reflar-gamification.forum.user.points",{points:this.props.user.data.attributes.Points}),t.add("points",e)}),Object(r.extend)(v.a.prototype,"view",function(n){var e=this.props.user,r=function n(e,r){var o=[];if(e.children){var s=e.children.find(t(r));void 0!==s&&(o=o.concat(s)),e.children.forEach(function(t){o=o.concat(n(t,r))})}return o}(n,"UserCard-profile")[0],o=r.children.find(t("UserCard-badges"));return e.ranks()&&(void 0===o||""===o?r.children.splice(1,0,m("ul",{className:"UserCard-badges badges"},e.ranks().reverse().map(function(t,n){if(!(n>=app.forum.attribute("ranksAmt")&&null!==app.forum.attribute("ranksAmt")))return m("li",{className:"User-Rank"},y(t))}))):o.children.push(e.ranks().reverse().map(function(t,n){if(!(n>=app.forum.attribute("ranksAmt")&&null!==app.forum.attribute("ranksAmt")))return m("li",{className:"User-Rank"},y(t))}))),n}),Object(r.extend)(d.a.prototype,"view",function(t){var n,e=this.props.post.user();return e?(t.children.find((n="h3",function(t){return t&&t.tag&&t.tag===n})).children.push(e.ranks().reverse().map(function(t,n){if(!(n>=app.forum.attribute("ranksAmt")&&null!==app.forum.attribute("ranksAmt")))return m("span",{className:"Post-Rank"},y(t))})),t):t})},x=e(5),N=e.n(x),P=e(13),O=e.n(P),j=e(19),V=e.n(j),I=e(26),M=e.n(I),w=e(3),C=e.n(w),R=e(27),S=e.n(R),L=function(){N.a.prototype.viewItems=function(){var t=this,n=new O.a,e=app.cache.discussionList.sortMap(),r={};for(var o in e)r[o]=app.translator.trans("core.forum.index_sort."+o+"_button");var s=r[this.params().sort]||Object.keys(e).map(function(t){return r[t]})[0];return/^.*?\/hot/.test(m.route())&&(s=app.translator.trans("core.forum.index_sort.hot_button")),n.add("sort",M.a.component({buttonClassName:"Button",label:s,children:Object.keys(r).map(function(n){var o=r[n],s=(t.params().sort||Object.keys(e)[0])===n;return/^.*?\/hot/.test(m.route())&&"hot"===n&&(s=!0),/^.*?\/hot/.test(m.route())&&"latest"===n&&(s=!1,m.redraw()),C.a.component({children:o,icon:!s||"fas fa-check",onclick:t.changeSort.bind(t,n),active:s})})})),n},Object(r.extend)(N.a.prototype,"navItems",function(t){t.add("rankings",S.a.component({href:app.route("rankings"),children:app.translator.trans("reflar-gamification.forum.nav.name"),icon:"fas fa-trophy"}),80)}),N.a.prototype.changeSort=function(t){var n=this.params();"hot"===t?(m.route("/"),m.route(m.route()+"hot")):(t===Object.keys(app.cache.discussionList.sortMap())[0]?delete n.sort:n.sort=t,"hot"===n.filter&&delete n.filter,m.route(app.route("index",n)))},Object(r.extend)(V.a.prototype,"sortMap",function(t){t.hot="hot"}),Object(r.extend)(V.a.prototype,"requestParams",function(t){"hot"===this.props.params.filter&&(t.filter.q=" is:hot")})},A=e(10),B=e.n(A),q=e(20),T=e.n(q),z=e(28),F=e.n(z),U=e(4),E=e(29),G=e.n(E),H=e(11),J=e.n(H),K=e(12),Q=e.n(K),W=function(t){function n(){return t.apply(this,arguments)||this}Object(U.a)(n,t);var e=n.prototype;return e.className=function(){return"VotesModal Modal--small"},e.title=function(){return app.translator.trans("reflar-gamification.forum.modal.title")},e.content=function(){return m("div",{className:"Modal-body"},m("ul",{className:"VotesModal-list"},m("legend",null,app.translator.trans("reflar-gamification.forum.modal.upvotes_label")),this.props.post.upvotes().map(function(t){return m("li",null,m("a",{href:app.route.user(t),config:m.route},J()(t)," "," ",Q()(t)))}),m("legend",null,app.translator.trans("reflar-gamification.forum.modal.downvotes_label")),this.props.post.downvotes().map(function(t){return m("li",null,m("a",{href:app.route.user(t),config:m.route},J()(t)," "," ",Q()(t)))})))},n}(G.a),X=e(7),Y=e(9),Z=e.n(Y),$=e(30),D=e.n($),_=e(31),tt=e.n(_),nt=function(t){function n(){return t.apply(this,arguments)||this}Object(U.a)(n,t);var e=n.prototype;return e.init=function(){t.prototype.init.call(this),app.session.user&&!0===app.session.user.data.attributes.canViewRankingPage||m.route("/"),this.loading=!0,this.users=[],this.refresh()},e.view=function(){var t,n=this;return t=this.loading?D.a.component():C.a.component({children:app.translator.trans("core.forum.discussion_list.load_more_button"),className:"Button",onclick:this.loadMore.bind(this)}),m("div",{className:"TagsPage"},N.a.prototype.hero(),m("div",{className:"container"},m("nav",{className:"RankingPage-nav IndexPage-nav sideNav",config:N.a.prototype.affixSidebar},m("ul",null,tt()(N.a.prototype.sidebarItems().toArray()))),m("div",{className:"RankingPage sideNavOffset"},m("table",{class:"rankings"},m("tr",null,m("th",{className:"rankings-mobile"},app.translator.trans("reflar-gamification.forum.ranking.rank")),m("th",null,app.translator.trans("reflar-gamification.forum.ranking.name")),m("th",null,app.translator.trans("reflar-gamification.forum.ranking.amount"))),this.users.map(function(t,e){return++e,[m("tr",{className:"ranking-"+e},e<4?"1"==app.forum.attribute("CustomRankingImages")?m("img",{className:"rankings-mobile rankings-image",src:app.forum.attribute("baseUrl")+app.forum.attribute("topimage"+e+"Url")}):m("td",{className:"rankings-mobile rankings-"+e},m("i",{className:"icon fas fa-trophy"})):m("td",{className:"rankings-4 rankings-mobile"},n.addOrdinalSuffix(e)),m("td",null,m("div",{className:"PostUser"},m("h3",{className:"rankings-info"},m("a",{href:app.route.user(t),config:m.route},e<4?J()(t,{className:"info-avatar rankings-"+e+"-avatar"}):""," ",Q()(t))))),e<4?m("td",{className:"rankings-"+e},t.data.attributes.Points):m("td",{className:"rankings-4"},t.data.attributes.Points))]})),m("div",{className:"rankings-loadmore"}," ",t))))},e.refresh=function(t){var n=this;return void 0===t&&(t=!0),t&&(this.loading=!0,this.users=[]),this.loadResults().then(function(t){n.users=[],n.parseResults(t)},function(){n.loading=!1,m.redraw()})},e.addOrdinalSuffix=function(t){if("en"==app.forum.attribute("DefaultLocale")){var n=t%10,e=t%100;return 1==n&&11!=e?t+"st":2==n&&12!=e?t+"nd":3==n&&13!=e?t+"rd":t+"th"}return t},e.stickyParams=function(){return{sort:m.route.param("sort"),q:m.route.param("q")}},e.actionItems=function(){var t=new O.a;return t.add("refresh",C.a.component({title:app.translator.trans("core.forum.index.refresh_tooltip"),icon:"fas fa-refresh",className:"Button Button--icon",onclick:function(){app.cache.discussionList.refresh(),app.session.user&&(app.store.find("users",app.session.user.id()),m.redraw())}})),t},e.newDiscussion=function(){var t=m.deferred();return app.session.user?this.composeNewDiscussion(t):app.modal.show(new B.a({onlogin:this.composeNewDiscussion.bind(this,t)})),t.promise},e.composeNewDiscussion=function(t){var n=new DiscussionComposer({user:app.session.user});return app.composer.load(n),app.composer.show(),t.resolve(n),t.promise},e.loadResults=function(t){var n={};return n.page={offset:t,limit:"10"},app.store.find("rankings",n)},e.loadMore=function(){this.loading=!0,this.loadResults(this.users.length).then(this.parseResults.bind(this))},e.parseResults=function(t){return[].push.apply(this.users,t),this.loading=!1,this.users.sort(function(t,n){return parseFloat(n.data.attributes.Points)-parseFloat(t.data.attributes.Points)}),m.lazyRedraw(),t},n}(Z.a),et=e(32),rt=function(t){function n(){return t.apply(this,arguments)||this}Object(U.a)(n,t);var e=n.prototype;return e.icon=function(){return"Up"===this.props.notification.content()?"fas fa-thumbs-up":"fas fa-thumbs-down"},e.href=function(){return app.route.post(this.props.notification.subject())},e.content=function(){var t=this.props.notification.fromUser().username();return"Up"===this.props.notification.content()?app.translator.trans("reflar-gamification.forum.notification.upvote",{username:t}):app.translator.trans("reflar-gamification.forum.notification.downvote",{username:t})},e.excerpt=function(){return this.props.notification.subject().contentPlain()},n}(e.n(et).a);s.a.initializers.add("Reflar-Gamification",function(t){t.store.models.ranks=X.a,t.notificationComponents.vote=rt,t.routes.rankings={path:"/rankings",component:nt.component()},Object(r.extend)(T.a.prototype,"config",function(t,n,e){var o=this;n||s.a.pusher&&s.a.pusher.then(function(t){t.main.bind("newVote",function(t){var n=parseInt(t.userId);if(n!=s.a.session.user.id()){if(m.startComputation(),o.postId()==t.postId){var e=o.upvotedata(),r=o.downvotedata();switch(t.before){case"up":e=o.removeVote(e,n);break;case"down":r=o.removeVote(r,n)}switch(t.after){case"up":e.unshift({type:"users",id:n});break;case"down":r.unshift({type:"users",id:n});break;case"none":r=o.removeVote(r,n),e=o.removeVote(e,n)}o.downvotedata(r),o.upvotedata(e),m.redraw.strategy("all")}m.endComputation()}}),Object(r.extend)(e,"onunload",function(){return t.main.unbind("newVote")})})}),Object(r.extend)(F.a,"moderationControls",function(t,n){n.discussion().canSeeVotes()&&t.add("viewVotes",[m(C.a,{icon:"fas fa-thumbs-up",onclick:function(){s.a.modal.show(new W({post:n}))}},s.a.translator.trans("reflar-gamification.forum.mod_item"))])}),Object(r.extend)(T.a.prototype,"actionItems",function(t){var n=this.props.post;this.postId=m.prop(n.data.id),this.downvotedata=m.prop(n.data.relationships.downvotes.data),this.upvotedata=m.prop(n.data.relationships.upvotes.data);var e=s.a.session.user&&n.upvotes().some(function(t){return t===s.a.session.user}),r=s.a.session.user&&n.downvotes().some(function(t){return t===s.a.session.user});s.a.session.user||(r=!1,e=!1);var o=s.a.forum.attribute("IconName");null!==o&&""!==o||(o="thumbs"),this.removeVote=function(t,n){return t.some(function(e,r){e.id==n&&t.splice(r,1)}),t},t.add("upvote",C.a.component({icon:"fas fa-"+o+"-up",className:"Post-vote Post-upvote",style:!1!==e?"color:"+s.a.forum.data.attributes.themePrimaryColor:"color:",disabled:!n.discussion().canVote(),onclick:function(){if(s.a.session.user){if(n.discussion().canVote()){var t=n.data.relationships.upvotes.data,o=n.data.relationships.downvotes.data;e=!e,r=!1,n.save([e,r,"vote"]),t.some(function(n,e){if(n.id===s.a.session.user.id())return t.splice(e,1),!0}),o.some(function(t,n){if(t.id===s.a.session.user.id())return o.splice(n,1),!0}),e&&t.unshift({type:"users",id:s.a.session.user.id()})}}else s.a.modal.show(new B.a)}}),3),t.add("points",m("label",{className:"Post-points"},this.upvotedata().length-this.downvotedata().length),2),t.add("downvote",C.a.component({icon:"fas fa-"+o+"-down",className:"Post-vote Post-downvote",style:!1!==r?"color:"+s.a.forum.data.attributes.themePrimaryColor:"",disabled:!n.discussion().canVote(),onclick:function(){if(s.a.session.user){if(n.discussion().canVote()){var t=n.data.relationships.upvotes.data,o=n.data.relationships.downvotes.data;r=!r,e=!1,n.save([e,r,"vote"]),t.some(function(n,e){if(n.id===s.a.session.user.id())return t.splice(e,1),!0}),o.some(function(t,n){if(t.id===s.a.session.user.id())return o.splice(n,1),!0}),r&&o.unshift({type:"users",id:s.a.session.user.id()})}}else s.a.modal.show(new B.a)}}),1)}),L(),k()})},,function(t,n){t.exports=flarum.core.compat["components/PostUser"]},function(t,n){t.exports=flarum.core.compat["components/Dropdown"]},function(t,n){t.exports=flarum.core.compat["components/LinkButton"]},function(t,n){t.exports=flarum.core.compat["utils/PostControls"]},function(t,n){t.exports=flarum.core.compat["components/Modal"]},function(t,n){t.exports=flarum.core.compat["components/LoadingIndicator"]},function(t,n){t.exports=flarum.core.compat["helpers/listItems"]},function(t,n){t.exports=flarum.core.compat["components/Notification"]},,,,,function(t,n,e){"use strict";e.r(n);var r=e(6);for(var o in r)"default"!==o&&function(t){e.d(n,t,function(){return r[t]})}(o);e(23)}]); 2 | //# sourceMappingURL=forum.js.map -------------------------------------------------------------------------------- /js/forum.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Flarum. 3 | * 4 | * (c) Toby Zerner 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export * from './src/common'; 11 | export * from './src/forum'; -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@reflar/gamification", 4 | "dependencies": { 5 | "flarum-webpack-config": "0.1.0-beta.10", 6 | "webpack": "^4.26.0" 7 | }, 8 | "scripts": { 9 | "dev": "webpack --mode development --watch", 10 | "build": "webpack --mode production" 11 | }, 12 | "devDependencies": { 13 | "webpack-cli": "^3.1.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /js/src/admin/addSettingsPage.js: -------------------------------------------------------------------------------- 1 | import {extend} from "flarum/extend"; 2 | import AdminNav from "flarum/components/AdminNav"; 3 | import AdminLinkButton from "flarum/components/AdminLinkButton"; 4 | import SettingsPage from "./components/SettingsPage"; 5 | 6 | export default function () { 7 | app.routes['reflar-gamification'] = {path: '/reflar/gamification', component: SettingsPage.component()}; 8 | 9 | app.extensionSettings['reflar-gamification'] = () => m.route(app.route('reflar-gamification')); 10 | 11 | extend(AdminNav.prototype, 'items', items => { 12 | items.add('reflar-gamification', AdminLinkButton.component({ 13 | href: app.route('reflar-gamification'), 14 | icon: 'fas fa-thumbs-up', 15 | children: 'Gamification', 16 | description: app.translator.trans('reflar-gamification.admin.nav.desc') 17 | })); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /js/src/admin/components/SettingsPage.js: -------------------------------------------------------------------------------- 1 | import Alert from "flarum/components/Alert"; 2 | import Page from 'flarum/components/Page'; 3 | import Button from "flarum/components/Button"; 4 | import UploadImageButton from 'flarum/components/UploadImageButton'; 5 | import saveSettings from "flarum/utils/saveSettings"; 6 | import Switch from "flarum/components/Switch"; 7 | 8 | export default class SettingsPage extends Page { 9 | 10 | init() { 11 | 12 | this.fields = [ 13 | 'convertedLikes', 14 | 'amountPerPost', 15 | 'amountPerDiscussion', 16 | 'postStartAmount', 17 | 'rankAmt', 18 | 'iconName', 19 | 'blockedUsers', 20 | 'pointsPlaceholder' 21 | ]; 22 | 23 | this.switches = [ 24 | 'autoUpvotePosts', 25 | 'customRankingImages' 26 | ]; 27 | 28 | this.ranks = app.store.all('ranks'); 29 | 30 | this.values = {}; 31 | 32 | this.settingsPrefix = 'reflar.gamification'; 33 | 34 | const settings = app.data.settings; 35 | 36 | this.fields.forEach(key => 37 | this.values[key] = m.prop(settings[this.addPrefix(key)]) 38 | ); 39 | 40 | this.switches.forEach(key => 41 | this.values[key] = m.prop(settings[this.addPrefix(key)] === '1') 42 | ); 43 | 44 | this.newRank = { 45 | 'points': m.prop(''), 46 | 'name': m.prop(''), 47 | 'color': m.prop('') 48 | }; 49 | } 50 | 51 | /** 52 | * @returns {*} 53 | */ 54 | view() { 55 | return [ 56 | m('div', {className: 'SettingsPage'}, [ 57 | m('div', {className: 'container'}, [ 58 | m('form', {onsubmit: this.onsubmit.bind(this)}, [ 59 | m('div', {className: 'helpText'}, app.translator.trans('reflar-gamification.admin.page.convert.help')), 60 | (this.values.convertedLikes() === undefined ? ( 61 | Button.component({ 62 | type: 'button', 63 | className: 'Button Button--warning Ranks-button', 64 | children: app.translator.trans('reflar-gamification.admin.page.convert.button'), 65 | onclick: () => { 66 | app.request({ 67 | url: app.forum.attribute('apiUrl') + '/reflar/gamification/convert', 68 | method: 'POST' 69 | }).then(this.values.convertedLikes('converting')); 70 | } 71 | }) 72 | ) : (this.values.convertedLikes() === 'converting' ? ( 73 | m('label', {}, app.translator.trans('reflar-gamification.admin.page.convert.converting')) 74 | ) : (m('label', {}, app.translator.trans('reflar-gamification.admin.page.convert.converted', {number: this.values.convertedLikes()}))))), 75 | 76 | m('fieldset', {className: 'SettingsPage-ranks'}, [ 77 | m('legend', {}, app.translator.trans('reflar-gamification.admin.page.ranks.title')), 78 | m('label', {}, app.translator.trans('reflar-gamification.admin.page.ranks.ranks')), 79 | m('div', {className: 'helpText'}, app.translator.trans('reflar-gamification.admin.page.ranks.help.help')), 80 | m('div', {className: 'Ranks--Container'}, 81 | this.ranks.map(rank => { 82 | return m('div', {style: "float: left;"}, [ 83 | m('input', { 84 | className: 'FormControl Ranks-number', 85 | type: 'number', 86 | value: rank.points(), 87 | placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.points'), 88 | oninput: m.withAttr('value', this.updatePoints.bind(this, rank)) 89 | }), 90 | m('input', { 91 | className: 'FormControl Ranks-name', 92 | value: rank.name(), 93 | placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.name'), 94 | oninput: m.withAttr('value', this.updateName.bind(this, rank)) 95 | }), 96 | m('input', { 97 | className: 'FormControl Ranks-color', 98 | value: rank.color(), 99 | placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.color'), 100 | oninput: m.withAttr('value', this.updateColor.bind(this, rank)) 101 | }), 102 | Button.component({ 103 | type: 'button', 104 | className: 'Button Button--warning Ranks-button', 105 | icon: 'fa fa-times', 106 | onclick: this.deleteRank.bind(this, rank) 107 | }), 108 | ]) 109 | }), 110 | m('div', {style: "float: left; margin-bottom: 15px"}, [ 111 | m('input', { 112 | className: 'FormControl Ranks-number', 113 | value: this.newRank.points(), 114 | placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.points'), 115 | type: 'number', 116 | oninput: m.withAttr('value', this.newRank.points) 117 | }), 118 | m('input', { 119 | className: 'FormControl Ranks-name', 120 | value: this.newRank.name(), 121 | placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.name'), 122 | oninput: m.withAttr('value', this.newRank.name) 123 | }), 124 | m('input', { 125 | className: 'FormControl Ranks-color', 126 | value: this.newRank.color(), 127 | placeholder: app.translator.trans('reflar-gamification.admin.page.ranks.help.color'), 128 | oninput: m.withAttr('value', this.newRank.color) 129 | } 130 | ), 131 | Button.component({ 132 | type: 'button', 133 | className: 'Button Button--warning Ranks-button', 134 | icon: 'fa fa-plus', 135 | onclick: this.addRank.bind(this) 136 | }), 137 | ]) 138 | ), 139 | m('label', {}, app.translator.trans('reflar-gamification.admin.page.ranks.number_title')), 140 | m('input', { 141 | className: 'FormControl Ranks-default', 142 | value: this.values.rankAmt() || '', 143 | placeholder: 2, 144 | oninput: m.withAttr('value', this.values.rankAmt) 145 | }), 146 | m('legend', {}, app.translator.trans('reflar-gamification.admin.page.votes.title')), 147 | m('label', {}, app.translator.trans('reflar-gamification.admin.page.votes.icon_name')), 148 | m('div', {className: 'helpText'}, app.translator.trans('reflar-gamification.admin.page.votes.icon_help')), 149 | m('input', { 150 | className: 'FormControl Ranks-default', 151 | value: this.values.iconName() || '', 152 | placeholder: 'thumbs', 153 | oninput: m.withAttr('value', this.values.iconName) 154 | }), 155 | Switch.component({ 156 | state: this.values.autoUpvotePosts() || false, 157 | children: app.translator.trans('reflar-gamification.admin.page.votes.auto_upvote'), 158 | onchange: this.values.autoUpvotePosts, 159 | className: 'votes-switch' 160 | }), 161 | m('label', {}, app.translator.trans('reflar-gamification.admin.page.votes.points_title')), 162 | m('input', { 163 | className: 'FormControl Ranks-default', 164 | value: this.values.pointsPlaceholder() || '', 165 | placeholder: app.translator.trans('reflar-gamification.admin.page.votes.points_placeholder') + '{points}', 166 | oninput: m.withAttr('value', this.values.pointsPlaceholder) 167 | }), 168 | m('legend', {}, app.translator.trans('reflar-gamification.admin.page.rankings.title')), 169 | Switch.component({ 170 | state: this.values.customRankingImages() || false, 171 | children: app.translator.trans('reflar-gamification.admin.page.rankings.enable'), 172 | onchange: this.values.customRankingImages, 173 | className: 'votes-switch' 174 | }), 175 | m('label', {}, app.translator.trans('reflar-gamification.admin.page.rankings.blocked.title')), 176 | m('input', { 177 | className: 'FormControl Ranks-blocked', 178 | placeholder: app.translator.trans('reflar-gamification.admin.page.rankings.blocked.placeholder'), 179 | value: this.values.blockedUsers() || '', 180 | oninput: m.withAttr('value', this.values.blockedUsers) 181 | }), 182 | m('div', { className: 'helpText' }, app.translator.trans('reflar-gamification.admin.page.rankings.blocked.help')), 183 | m('label', { className: 'Upload-label' }, app.translator.trans('reflar-gamification.admin.page.rankings.custom_image_1')), 184 | , 185 | m('br'), 186 | m('label', { className: 'Upload-label' }, app.translator.trans('reflar-gamification.admin.page.rankings.custom_image_2')), 187 | , 188 | m('br'), 189 | m('label', { className: 'Upload-label' }, app.translator.trans('reflar-gamification.admin.page.rankings.custom_image_3')), 190 | , 191 | m('br'), 192 | Button.component({ 193 | type: 'submit', 194 | className: 'Button Button--primary Ranks-save', 195 | children: app.translator.trans('reflar-gamification.admin.page.save_settings'), 196 | loading: this.loading, 197 | disabled: !this.changed() 198 | }) 199 | ]) 200 | ]) 201 | ]) 202 | ]) 203 | ]; 204 | } 205 | 206 | 207 | updateName(rank, value) { 208 | rank.save({name: value}); 209 | } 210 | 211 | updatePoints(rank, value) { 212 | rank.save({points: value}); 213 | } 214 | 215 | updateColor(rank, value) { 216 | rank.save({color: value}); 217 | } 218 | 219 | deleteRank(rankToDelete) { 220 | rankToDelete.delete(); 221 | this.ranks.some((rank, i) => { 222 | if (rank.data.id === rankToDelete.data.id) { 223 | this.ranks.splice(i, 1); 224 | return true; 225 | } 226 | }) 227 | } 228 | 229 | addRank(rank) { 230 | app.store.createRecord('ranks').save({ 231 | points: this.newRank.points(), 232 | name: this.newRank.name(), 233 | color: this.newRank.color() 234 | }).then( 235 | rank => { 236 | this.newRank.color(''); 237 | this.newRank.name(''); 238 | this.newRank.points(''); 239 | this.ranks.push(rank); 240 | m.redraw(); 241 | } 242 | ); 243 | } 244 | 245 | 246 | /** 247 | * 248 | * @returns boolean 249 | */ 250 | changed() { 251 | var switchesCheck = this.switches.some(key => this.values[key]() !== (app.data.settings[this.addPrefix(key)] == '1')); 252 | var fieldsCheck = this.fields.some(key => this.values[key]() !== app.data.settings[this.addPrefix(key)]); 253 | return fieldsCheck || switchesCheck; 254 | } 255 | 256 | /** 257 | * @param e 258 | */ 259 | onsubmit(e) { 260 | e.preventDefault(); 261 | 262 | if (this.loading) return; 263 | 264 | this.loading = true; 265 | 266 | app.alerts.dismiss(this.successAlert); 267 | 268 | const settings = {}; 269 | 270 | this.switches.forEach(key => settings[this.addPrefix(key)] = this.values[key]()); 271 | this.fields.forEach(key => settings[this.addPrefix(key)] = this.values[key]()); 272 | 273 | saveSettings(settings) 274 | .then(() => { 275 | app.alerts.show(this.successAlert = new Alert({ 276 | type: 'success', 277 | children: app.translator.trans('core.admin.basics.saved_message') 278 | })); 279 | }) 280 | .catch(() => { 281 | }) 282 | .then(() => { 283 | this.loading = false; 284 | window.location.reload(); 285 | }); 286 | } 287 | 288 | /** 289 | * @returns string 290 | */ 291 | addPrefix(key) { 292 | return this.settingsPrefix + '.' + key; 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /js/src/admin/index.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/app' 2 | import {extend} from 'flarum/extend' 3 | import PermissionGrid from 'flarum/components/PermissionGrid' 4 | import addSettingsPage from './addSettingsPage' 5 | import Rank from '../common/models/Rank' 6 | 7 | app.initializers.add('reflar-gamification', app => { 8 | app.store.models.ranks = Rank; 9 | 10 | extend(PermissionGrid.prototype, 'replyItems', items => { 11 | items.add('Vote', { 12 | icon: 'fas fa-thumbs-up', 13 | label: app.translator.trans('reflar-gamification.admin.permissions.vote_label'), 14 | permission: 'discussion.vote' 15 | }) 16 | }); 17 | 18 | extend(PermissionGrid.prototype, 'viewItems', items => { 19 | items.add('canSeeVotes', { 20 | icon: 'fas fa-info-circle', 21 | label: app.translator.trans('reflar-gamification.admin.permissions.see_votes_label'), 22 | permission: 'discussion.canSeeVotes' 23 | }); 24 | items.add('canViewRankingPage', { 25 | icon: 'fas fa-trophy', 26 | label: app.translator.trans('reflar-gamification.admin.permissions.see_ranking_page'), 27 | permission: 'reflar.gamification.viewRankingPage', 28 | allowGuest: true 29 | }) 30 | }); 31 | 32 | addSettingsPage(); 33 | }); 34 | -------------------------------------------------------------------------------- /js/src/common/helpers/rankLabel.js: -------------------------------------------------------------------------------- 1 | export default function rankLabel(rank, attrs = {}) { 2 | attrs.style = attrs.style || {}; 3 | attrs.className = 'rankLabel ' + (attrs.className || ''); 4 | 5 | const color = rank.color(); 6 | attrs.style.backgroundColor = attrs.style.color = color; 7 | attrs.className += ' colored'; 8 | 9 | return ( 10 | m('span', attrs, 11 | 12 | {rank.name()} 13 | 14 | ) 15 | ); 16 | } -------------------------------------------------------------------------------- /js/src/common/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReFlar/gamification/a90ecc2d3ddfe54d200b3192ae25b69df952b63b/js/src/common/index.js -------------------------------------------------------------------------------- /js/src/common/models/Rank.js: -------------------------------------------------------------------------------- 1 | import Model from 'flarum/Model'; 2 | import mixin from 'flarum/utils/mixin'; 3 | 4 | export default class Rank extends mixin(Model, { 5 | points: Model.attribute('points'), 6 | name: Model.attribute('name'), 7 | color: Model.attribute('color') 8 | }) {} -------------------------------------------------------------------------------- /js/src/forum/components/AddAttributes.js: -------------------------------------------------------------------------------- 1 | import Discussion from "flarum/models/Discussion"; 2 | import {extend} from "flarum/extend"; 3 | import Model from "flarum/Model"; 4 | import Post from "flarum/models/Post"; 5 | import PostUser from "flarum/components/PostUser"; 6 | import User from "flarum/models/User"; 7 | import UserCard from "flarum/components/UserCard"; 8 | import rankLabel from "../../common/helpers/rankLabel"; 9 | 10 | export default function () { 11 | Discussion.prototype.canVote = Model.attribute('canVote'); 12 | Discussion.prototype.canSeeVotes = Model.attribute('canSeeVotes'); 13 | Discussion.prototype.votes = Model.attribute('votes'); 14 | 15 | User.prototype.points = Model.attribute('points'); 16 | User.prototype.ranks = Model.hasMany('ranks'); 17 | 18 | Post.prototype.upvotes = Model.hasMany('upvotes'); 19 | Post.prototype.downvotes = Model.hasMany('downvotes'); 20 | 21 | const matchClass = className => { 22 | return node => node && node.attrs && node.attrs.className && node.attrs.className === className; 23 | }; 24 | 25 | const matchTag = tagName => { 26 | return node => node && node.tag && node.tag === tagName; 27 | }; 28 | 29 | const findMatchClass = function(node, className) { 30 | var newArray = []; 31 | if(node.children) { 32 | var nodeInChildren = node.children.find(matchClass(className)); 33 | if(nodeInChildren !== undefined) { 34 | newArray = newArray.concat(nodeInChildren); 35 | } 36 | node.children.forEach(function(currentValue) { 37 | newArray = newArray.concat(findMatchClass(currentValue, className)); 38 | }); 39 | } 40 | return newArray; 41 | }; 42 | 43 | extend(UserCard.prototype, 'infoItems', function (items, user) { 44 | let points = ''; 45 | 46 | if (points == 0) { 47 | points = '0'; 48 | } 49 | 50 | if (app.forum.attribute('PointsPlaceholder')) { 51 | points = app.forum.attribute('PointsPlaceholder').replace('{points}', this.props.user.data.attributes.Points); 52 | } else { 53 | points = app.translator.trans('reflar-gamification.forum.user.points', {points: this.props.user.data.attributes.Points}); 54 | } 55 | 56 | items.add('points', 57 | points 58 | ); 59 | }); 60 | 61 | extend(UserCard.prototype, 'view', function (vnode) { 62 | const user = this.props.user; 63 | let profile_node = findMatchClass(vnode, 'UserCard-profile')[0]; 64 | let badges_node = profile_node.children.find(matchClass('UserCard-badges')); 65 | if(user.ranks()) { 66 | if(badges_node === undefined || badges_node === "") { 67 | profile_node.children.splice(1, 0, ( 68 |
    69 | {user.ranks().reverse().map((rank, i) => { 70 | if (i >= app.forum.attribute('ranksAmt') && app.forum.attribute('ranksAmt') !== null) { 71 | 72 | } else { 73 | return ( 74 |
  • 75 | {rankLabel(rank)} 76 |
  • 77 | ); 78 | } 79 | })} 80 |
81 | )) 82 | } else { 83 | badges_node.children.push(user.ranks().reverse().map((rank, i) => { 84 | if (i >= app.forum.attribute('ranksAmt') && app.forum.attribute('ranksAmt') !== null) { 85 | 86 | } else { 87 | return ( 88 |
  • 89 | {rankLabel(rank)} 90 |
  • 91 | ); 92 | } 93 | })); 94 | } 95 | } 96 | 97 | return vnode; 98 | }); 99 | 100 | extend(PostUser.prototype, 'view', function(vnode) { 101 | const post = this.props.post; 102 | const user = post.user(); 103 | 104 | if (!user) { 105 | return vnode; 106 | } 107 | 108 | const header_node = vnode.children.find(matchTag('h3')); 109 | header_node.children.push(user.ranks().reverse().map((rank, i) => { 110 | if (i >= app.forum.attribute('ranksAmt') && app.forum.attribute('ranksAmt') !== null) { 111 | 112 | } else { 113 | return ( 114 | {rankLabel(rank)} 115 | ); 116 | } 117 | })); 118 | 119 | return vnode; 120 | }); 121 | } -------------------------------------------------------------------------------- /js/src/forum/components/AddHotnessSort.js: -------------------------------------------------------------------------------- 1 | import {extend} from 'flarum/extend' 2 | import IndexPage from 'flarum/components/IndexPage' 3 | import ItemList from 'flarum/utils/ItemList' 4 | import DiscussionList from 'flarum/components/DiscussionList' 5 | import Dropdown from 'flarum/components/Dropdown' 6 | import Button from 'flarum/components/Button' 7 | import LinkButton from 'flarum/components/LinkButton' 8 | 9 | export default function () { 10 | 11 | 12 | 13 | IndexPage.prototype.viewItems = function () { 14 | const items = new ItemList() 15 | const sortMap = app.cache.discussionList.sortMap() 16 | 17 | const sortOptions = {} 18 | for (const i in sortMap) { 19 | sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button') 20 | } 21 | 22 | let dropDownLabel = sortOptions[this.params().sort] || Object.keys(sortMap).map(key => sortOptions[key])[0] 23 | 24 | if (/^.*?\/hot/.test(m.route())) { 25 | dropDownLabel = app.translator.trans('core.forum.index_sort.hot_button') 26 | } 27 | 28 | items.add('sort', 29 | Dropdown.component({ 30 | buttonClassName: 'Button', 31 | label: dropDownLabel, 32 | children: Object.keys(sortOptions).map(value => { 33 | const label = sortOptions[value] 34 | let active = (this.params().sort || Object.keys(sortMap)[0]) === value 35 | 36 | if (/^.*?\/hot/.test(m.route()) && value === 'hot') { 37 | active = true 38 | } 39 | 40 | if (/^.*?\/hot/.test(m.route()) && value === 'latest') { 41 | active = false 42 | m.redraw() 43 | } 44 | 45 | return Button.component({ 46 | children: label, 47 | icon: active ? 'fas fa-check' : true, 48 | onclick: this.changeSort.bind(this, value), 49 | active: active 50 | }) 51 | }) 52 | }) 53 | ) 54 | 55 | return items 56 | } 57 | 58 | extend(IndexPage.prototype, 'navItems', function (items) { 59 | items.add('rankings', 60 | LinkButton.component({ 61 | href: app.route('rankings'), 62 | children: app.translator.trans('reflar-gamification.forum.nav.name'), 63 | icon: 'fas fa-trophy' 64 | }), 65 | 80 66 | ) 67 | }); 68 | 69 | IndexPage.prototype.changeSort = function (sort) { 70 | const params = this.params() 71 | 72 | if (sort === 'hot') { 73 | m.route('/') 74 | m.route(m.route() + 'hot') 75 | } else { 76 | if (sort === Object.keys(app.cache.discussionList.sortMap())[0]) { 77 | delete params.sort 78 | } else { 79 | params.sort = sort 80 | } 81 | if (params.filter === 'hot') { 82 | delete params.filter 83 | } 84 | m.route(app.route('index', params)) 85 | } 86 | } 87 | 88 | extend(DiscussionList.prototype, 'sortMap', function (map) { 89 | map.hot = 'hot' 90 | }) 91 | 92 | extend(DiscussionList.prototype, 'requestParams', function (params) { 93 | if (this.props.params.filter === 'hot') { 94 | params.filter.q = ' is:hot' 95 | } 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /js/src/forum/components/AddVoteButtons.js: -------------------------------------------------------------------------------- 1 | import {extend} from 'flarum/extend'; 2 | import app from 'flarum/app'; 3 | import Button from 'flarum/components/Button'; 4 | import LogInModal from 'flarum/components/LogInModal'; 5 | import CommentPost from 'flarum/components/CommentPost'; 6 | import PostControls from 'flarum/utils/PostControls'; 7 | import VotesModal from './VotesModal'; 8 | 9 | export default function () { 10 | 11 | extend(CommentPost.prototype, 'config', function (x, isInitialized, context) { 12 | if (isInitialized) return 13 | 14 | if (app.pusher) { 15 | app.pusher.then(channels => { 16 | channels.main.bind('newVote', data => { 17 | 18 | var userId = parseInt(data.userId) 19 | 20 | if (userId == app.session.user.id()) return 21 | 22 | m.startComputation() 23 | 24 | if (this.postId() == data.postId) { 25 | 26 | var upData = this.upvotedata() 27 | var downData = this.downvotedata() 28 | 29 | switch (data.before) { 30 | case 'up': 31 | upData = this.removeVote(upData, userId) 32 | break; 33 | case 'down': 34 | downData = this.removeVote(downData, userId) 35 | break; 36 | 37 | } 38 | 39 | switch (data.after) { 40 | case 'up': 41 | upData.unshift({type: 'users', id: userId}) 42 | break; 43 | case 'down': 44 | downData.unshift({type: 'users', id: userId}) 45 | break; 46 | case 'none': 47 | downData = this.removeVote(downData, userId) 48 | upData = this.removeVote(upData, userId) 49 | break; 50 | } 51 | 52 | this.downvotedata(downData) 53 | this.upvotedata(upData) 54 | 55 | m.redraw.strategy('all'); 56 | 57 | } 58 | 59 | m.endComputation() 60 | 61 | }) 62 | 63 | 64 | extend(context, 'onunload', () => channels.main.unbind('newVote')); 65 | }); 66 | } 67 | }) 68 | 69 | extend(PostControls, 'moderationControls', function (items, post) { 70 | if (post.discussion().canSeeVotes()) { 71 | items.add('viewVotes', [ 72 | m(Button, { 73 | icon: 'fas fa-thumbs-up', 74 | onclick: () => { 75 | app.modal.show(new VotesModal({post})) 76 | } 77 | }, app.translator.trans('reflar-gamification.forum.mod_item')) 78 | ]); 79 | } 80 | }); 81 | 82 | 83 | extend(CommentPost.prototype, 'actionItems', function (items) { 84 | const post = this.props.post 85 | 86 | this.postId = m.prop(post.data.id) 87 | 88 | this.downvotedata = m.prop(post.data.relationships.downvotes.data) 89 | this.upvotedata = m.prop(post.data.relationships.upvotes.data) 90 | 91 | let isUpvoted = app.session.user && post.upvotes().some(user => user === app.session.user) 92 | let isDownvoted = app.session.user && post.downvotes().some(user => user === app.session.user) 93 | 94 | if (!app.session.user) { 95 | isDownvoted = false 96 | isUpvoted = false 97 | } 98 | 99 | let icon = app.forum.attribute('IconName') 100 | 101 | if (icon === null || icon === '') { 102 | icon = 'thumbs' 103 | } 104 | 105 | this.removeVote = function (data, userId) { 106 | data.some((vote, i) => { 107 | if (vote.id == userId) { 108 | data.splice(i, 1) 109 | } 110 | }) 111 | return data 112 | } 113 | 114 | items.add('upvote', 115 | Button.component({ 116 | icon: 'fas fa-' + icon + '-up', 117 | className: 'Post-vote Post-upvote', 118 | style: isUpvoted !== false ? 'color:' + app.forum.data.attributes.themePrimaryColor : 'color:', 119 | disabled: !post.discussion().canVote(), 120 | onclick: () => { 121 | if (!app.session.user) { 122 | app.modal.show(new LogInModal()) 123 | return 124 | } 125 | if (!post.discussion().canVote()) return 126 | 127 | var upData = post.data.relationships.upvotes.data; 128 | var downData = post.data.relationships.downvotes.data; 129 | 130 | isUpvoted = !isUpvoted; 131 | 132 | isDownvoted = false; 133 | 134 | post.save([isUpvoted, isDownvoted, 'vote']); 135 | 136 | upData.some((upvote, i) => { 137 | if (upvote.id === app.session.user.id()) { 138 | upData.splice(i, 1); 139 | return true; 140 | } 141 | }); 142 | downData.some((downvote, i) => { 143 | if (downvote.id === app.session.user.id()) { 144 | downData.splice(i, 1); 145 | return true; 146 | } 147 | }); 148 | if (isUpvoted) { 149 | upData.unshift({type: 'users', id: app.session.user.id()}); 150 | } 151 | } 152 | }), 3 153 | ) 154 | 155 | items.add('points', 156 | 159 | , 2) 160 | 161 | items.add('downvote', 162 | Button.component({ 163 | icon: 'fas fa-' + icon + '-down', 164 | className: 'Post-vote Post-downvote', 165 | style: isDownvoted !== false ? 'color:' + app.forum.data.attributes.themePrimaryColor : '', 166 | disabled: !post.discussion().canVote(), 167 | onclick: () => { 168 | if (!app.session.user) { 169 | app.modal.show(new LogInModal()) 170 | return 171 | } 172 | if (!post.discussion().canVote()) return 173 | 174 | var upData = post.data.relationships.upvotes.data; 175 | var downData = post.data.relationships.downvotes.data 176 | 177 | isDownvoted = !isDownvoted; 178 | 179 | isUpvoted = false; 180 | 181 | post.save([isUpvoted, isDownvoted, 'vote']); 182 | 183 | upData.some((upvote, i) => { 184 | if (upvote.id === app.session.user.id()) { 185 | upData.splice(i, 1); 186 | return true; 187 | } 188 | }); 189 | downData.some((downvote, i) => { 190 | if (downvote.id === app.session.user.id()) { 191 | downData.splice(i, 1); 192 | return true; 193 | } 194 | }); 195 | 196 | if (isDownvoted) { 197 | downData.unshift({type: 'users', id: app.session.user.id()}); 198 | } 199 | } 200 | }), 1 201 | ) 202 | }) 203 | } 204 | -------------------------------------------------------------------------------- /js/src/forum/components/RankingsPage.js: -------------------------------------------------------------------------------- 1 | import {extend} from 'flarum/extend' 2 | import avatar from 'flarum/helpers/avatar' 3 | import Page from 'flarum/components/Page' 4 | import IndexPage from 'flarum/components/IndexPage' 5 | import Button from 'flarum/components/Button' 6 | import ItemList from 'flarum/utils/ItemList' 7 | import LogInModal from 'flarum/components/LogInModal' 8 | import LoadingIndicator from 'flarum/components/LoadingIndicator' 9 | import listItems from 'flarum/helpers/listItems' 10 | import username from 'flarum/helpers/username' 11 | 12 | export default class RankingsPage extends Page { 13 | init() { 14 | super.init() 15 | 16 | if (!app.session.user || app.session.user.data.attributes.canViewRankingPage !== true) { 17 | m.route('/') 18 | } 19 | 20 | this.loading = true 21 | this.users = [] 22 | this.refresh() 23 | } 24 | 25 | view() { 26 | let loading 27 | 28 | if (this.loading) { 29 | loading = LoadingIndicator.component() 30 | } else { 31 | loading = Button.component({ 32 | children: app.translator.trans('core.forum.discussion_list.load_more_button'), 33 | className: 'Button', 34 | onclick: this.loadMore.bind(this) 35 | }) 36 | } 37 | return ( 38 |
    39 | {IndexPage.prototype.hero()} 40 |
    41 | 44 |
    45 | 46 | 47 | 48 | 49 | 50 | 51 | {this.users.map((user, i) => { 52 | ++i 53 | return [ 54 | 55 | {i < 4 ? (app.forum.attribute('CustomRankingImages') == '1' ? ( 56 | ) 58 | : ( 59 | )) 61 | : ( 62 | )} 63 | 72 | {i < 4 ? ( 73 | ) 74 | : ( 75 | )} 76 | 77 | ] 78 | })} 79 |
    {app.translator.trans('reflar-gamification.forum.ranking.rank')}{app.translator.trans('reflar-gamification.forum.ranking.name')}{app.translator.trans('reflar-gamification.forum.ranking.amount')}
    60 | {this.addOrdinalSuffix(i)} 64 | 71 | {user.data.attributes.Points}{user.data.attributes.Points}
    80 |
    {loading}
    81 |
    82 |
    83 |
    84 | ) 85 | } 86 | 87 | refresh(clear = true) { 88 | if (clear) { 89 | this.loading = true 90 | this.users = [] 91 | } 92 | 93 | return this.loadResults().then( 94 | results => { 95 | this.users = [] 96 | this.parseResults(results) 97 | }, 98 | () => { 99 | this.loading = false 100 | m.redraw() 101 | } 102 | ) 103 | } 104 | 105 | addOrdinalSuffix(i) { 106 | if (app.forum.attribute('DefaultLocale') == 'en') { 107 | var j = i % 10, 108 | k = i % 100 109 | if (j == 1 && k != 11) { 110 | return i + 'st' 111 | } 112 | if (j == 2 && k != 12) { 113 | return i + 'nd' 114 | } 115 | if (j == 3 && k != 13) { 116 | return i + 'rd' 117 | } 118 | return i + 'th' 119 | } else { 120 | return i 121 | } 122 | } 123 | 124 | stickyParams() { 125 | return { 126 | sort: m.route.param('sort'), 127 | q: m.route.param('q') 128 | } 129 | } 130 | 131 | actionItems() { 132 | const items = new ItemList() 133 | 134 | items.add('refresh', 135 | Button.component({ 136 | title: app.translator.trans('core.forum.index.refresh_tooltip'), 137 | icon: 'fas fa-refresh', 138 | className: 'Button Button--icon', 139 | onclick: () => { 140 | app.cache.discussionList.refresh() 141 | if (app.session.user) { 142 | app.store.find('users', app.session.user.id()) 143 | m.redraw() 144 | } 145 | } 146 | }) 147 | ) 148 | 149 | return items 150 | } 151 | 152 | newDiscussion() { 153 | const deferred = m.deferred() 154 | 155 | if (app.session.user) { 156 | this.composeNewDiscussion(deferred) 157 | } else { 158 | app.modal.show( 159 | new LogInModal({ 160 | onlogin: this.composeNewDiscussion.bind(this, deferred) 161 | }) 162 | ) 163 | } 164 | 165 | return deferred.promise 166 | } 167 | 168 | composeNewDiscussion(deferred) { 169 | const component = new DiscussionComposer({user: app.session.user}) 170 | 171 | app.composer.load(component) 172 | app.composer.show() 173 | 174 | deferred.resolve(component) 175 | 176 | return deferred.promise 177 | } 178 | 179 | loadResults(offset) { 180 | const params = {} 181 | params.page = { 182 | offset: offset, 183 | limit: '10' 184 | } 185 | 186 | return app.store.find('rankings', params) 187 | } 188 | 189 | loadMore() { 190 | this.loading = true 191 | 192 | this.loadResults(this.users.length) 193 | .then(this.parseResults.bind(this)) 194 | } 195 | 196 | parseResults(results) { 197 | [].push.apply(this.users, results) 198 | 199 | this.loading = false 200 | 201 | this.users.sort(function (a, b) { 202 | return parseFloat(b.data.attributes.Points) - parseFloat(a.data.attributes.Points) 203 | }) 204 | 205 | m.lazyRedraw() 206 | 207 | return results 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /js/src/forum/components/VoteNotification.js: -------------------------------------------------------------------------------- 1 | import Notification from 'flarum/components/Notification' 2 | 3 | export default class UpvotedNotification extends Notification { 4 | icon() { 5 | if (this.props.notification.content() === 'Up') { 6 | return 'fas fa-thumbs-up' 7 | } else { 8 | return 'fas fa-thumbs-down' 9 | } 10 | } 11 | 12 | href() { 13 | return app.route.post(this.props.notification.subject()) 14 | } 15 | 16 | content() { 17 | let username = this.props.notification.fromUser().username(); 18 | 19 | if (this.props.notification.content() === 'Up') { 20 | return app.translator.trans('reflar-gamification.forum.notification.upvote', {username}) 21 | } else { 22 | return app.translator.trans('reflar-gamification.forum.notification.downvote', {username}) 23 | } 24 | } 25 | 26 | excerpt() { 27 | return this.props.notification.subject().contentPlain() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /js/src/forum/components/VotesModal.js: -------------------------------------------------------------------------------- 1 | import Modal from 'flarum/components/Modal'; 2 | import avatar from 'flarum/helpers/avatar'; 3 | import username from 'flarum/helpers/username'; 4 | 5 | export default class VotesModal extends Modal { 6 | className() { 7 | return 'VotesModal Modal--small'; 8 | } 9 | 10 | title() { 11 | return app.translator.trans('reflar-gamification.forum.modal.title'); 12 | } 13 | 14 | content() { 15 | return ( 16 |
    17 | 41 |
    42 | ); 43 | } 44 | } -------------------------------------------------------------------------------- /js/src/forum/index.js: -------------------------------------------------------------------------------- 1 | import {extend} from 'flarum/extend' 2 | import app from 'flarum/app' 3 | import AddAttributes from './components/AddAttributes' 4 | import AddHotnessFilter from './components/AddHotnessSort' 5 | import AddVoteButtons from './components/AddVoteButtons' 6 | import Rank from '../common/models/Rank' 7 | import RankingsPage from './components/RankingsPage' 8 | import VoteNotification from './components/VoteNotification' 9 | 10 | app.initializers.add('Reflar-Gamification', app => { 11 | app.store.models.ranks = Rank; 12 | 13 | app.notificationComponents.vote = VoteNotification; 14 | 15 | app.routes.rankings = {path: '/rankings', component: RankingsPage.component()}; 16 | 17 | AddVoteButtons(); 18 | AddHotnessFilter(); 19 | AddAttributes(); 20 | }); 21 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | const config = require('flarum-webpack-config'); 2 | 3 | module.exports = config(); 4 | -------------------------------------------------------------------------------- /migrations/2017_04_09_224815_create_posts_votes_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->create('posts_votes', function (Blueprint $table) { 18 | $table->increments('id'); 19 | $table->integer('post_id')->unsigned(); 20 | $table->integer('user_id')->unsigned(); 21 | $table->string('type'); 22 | }); 23 | }, 24 | 'down' => function (Builder $schema) { 25 | $schema->drop('posts_votes'); 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /migrations/2017_04_09_225024_add_votes_to_users.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->table('users', function (Blueprint $table) { 18 | $table->integer('votes'); 19 | $table->string('rank'); 20 | }); 21 | }, 22 | 'down' => function (Builder $schema) { 23 | $schema->table('users', function (Blueprint $table) { 24 | $table->dropColumn('votes'); 25 | $table->dropColumn('rank'); 26 | }); 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /migrations/2017_04_24_094425_add_hotness_to_discussions.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->table('discussions', function (Blueprint $table) { 18 | $table->integer('votes'); 19 | $table->float('hotness', 10, 4); 20 | }); 21 | }, 22 | 'down' => function (Builder $schema) { 23 | $schema->table('discussions', function (Blueprint $table) { 24 | $table->dropColumn('votes'); 25 | $table->dropColumn('hotness'); 26 | }); 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /migrations/2017_04_25__133721_add_default_vote_permissions.php: -------------------------------------------------------------------------------- 1 | Group::MEMBER_ID, 17 | ]); 18 | -------------------------------------------------------------------------------- /migrations/2017_04_26_202436_create_users_ranks_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->create('users_ranks', function (Blueprint $table) { 18 | $table->integer('user_id')->unsigned(); 19 | $table->integer('rank_id')->unsigned(); 20 | $table->primary(['user_id', 'rank_id']); 21 | }); 22 | }, 23 | 'down' => function (Builder $schema) { 24 | $schema->drop('users_ranks'); 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /migrations/2017_04_26_202644_create_ranks_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->create('ranks', function (Blueprint $table) { 18 | $table->increments('id'); 19 | $table->integer('points')->unsigned(); 20 | $table->string('name'); 21 | $table->string('color'); 22 | }); 23 | }, 24 | 'down' => function (Builder $schema) { 25 | $schema->drop('ranks'); 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /migrations/2017_08_11_225322_add_default_ranking_permission.php: -------------------------------------------------------------------------------- 1 | Group::GUEST_ID, 18 | ]); 19 | -------------------------------------------------------------------------------- /migrations/2017_09_05_214452_add_time_attribute_to_users.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | if (!$schema->hasColumn('users', 'last_vote_time')) { 18 | $schema->table('users', function (Blueprint $table) { 19 | $table->dateTime('last_vote_time'); 20 | }); 21 | } 22 | }, 23 | 'down' => function (Builder $schema) { 24 | $schema->table('users', function (Blueprint $table) { 25 | $table->dropColumn('last_vote_time'); 26 | }); 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /migrations/2018_08_02_110300_rename_users_ranks_to_rank_user.php: -------------------------------------------------------------------------------- 1 | ul { 21 | list-style: none; 22 | margin: 0; 23 | padding: 0; 24 | } 25 | } 26 | } 27 | 28 | .Ranks-blocked { 29 | width: 50%; 30 | margin-top: 5px; 31 | margin-bottom: 5px 32 | } 33 | 34 | .Upload-label { 35 | margin-right: 15px; 36 | } 37 | 38 | .Upload-button { 39 | margin-top: 7.5px; 40 | margin-bottom: 7.5px; 41 | } 42 | 43 | .helpText { 44 | margin-bottom: 20px; 45 | } 46 | 47 | .SettingsPage-ranks { 48 | margin-top: 20px; 49 | } 50 | 51 | .Ranks-number { 52 | width: 15%; 53 | float: left; 54 | margin: 5px 5px 5px; 55 | } 56 | .Ranks-name { 57 | width: 30%; 58 | float: left; 59 | margin: 5px 5px 5px; 60 | } 61 | .Ranks-color { 62 | width: 18%; 63 | float: left; 64 | margin: 5px 5px 5px; 65 | } 66 | .Ranks-button { 67 | margin: 5px 0 5px; 68 | } 69 | .Ranks-default { 70 | width: 30%; 71 | margin-top: 5px; 72 | margin-bottom: 5px 73 | } 74 | .Ranks-save { 75 | margin-top: 15px; 76 | } 77 | .votes-switch { 78 | margin-top: 20px; 79 | margin-bottom: 20px; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /resources/less/forum/extension.less: -------------------------------------------------------------------------------- 1 | @import "../lib/rankLabel.less"; 2 | 3 | .VotesModal-list { 4 | list-style: none; 5 | padding: 0; 6 | margin: 0; 7 | 8 | a { 9 | color: @text-color; 10 | font-size: 15px; 11 | font-weight: bold; 12 | display: block; 13 | margin-bottom: 10px; 14 | text-decoration: none; 15 | 16 | &:hover .username { 17 | text-decoration: underline; 18 | } 19 | } 20 | 21 | .Avatar { 22 | .Avatar--size(32px); 23 | vertical-align: middle; 24 | margin-right: 5px; 25 | } 26 | } 27 | 28 | @media only screen and (max-width: 529px) { 29 | .rankings-mobile { 30 | display: none !important; 31 | } 32 | } 33 | 34 | .rankings-mobile { 35 | width: 215px; 36 | } 37 | 38 | .Post-vote { 39 | background-color: rgba(0, 0, 0, 0); 40 | border: none; 41 | margin: 7px 1em 0 1em; 42 | } 43 | 44 | .Post-Rank { 45 | color: gray; 46 | font-size: 10px; 47 | margin-left: 10px; 48 | } 49 | 50 | .Post-vote:focus { 51 | outline: 0; 52 | } 53 | 54 | .item-points { 55 | margin-top: 7px; 56 | background-color: rgba(0, 0, 0, 0); 57 | border: none; 58 | margin-right: 0.5em !important; 59 | } 60 | 61 | .User-Rank { 62 | color: gray; 63 | font-size: 11px; 64 | margin-left: 11px; 65 | } 66 | 67 | .ranking-1, .ranking-2, .ranking-3 { 68 | span.username { 69 | margin-top: 15px; 70 | } 71 | 72 | span.username:hover { 73 | text-decoration: underline; 74 | } 75 | } 76 | 77 | .rankings { 78 | width: 100%; 79 | 80 | td, th { 81 | text-align: center; 82 | padding: 5px; 83 | } 84 | 85 | &-1 { 86 | font-size: 8em; 87 | color: gold; 88 | } 89 | 90 | &-1-avatar { 91 | width: 80px; 92 | height: 80px; 93 | font-size: 40px; 94 | line-height: 80px; 95 | } 96 | 97 | a:hover { 98 | text-decoration: none; 99 | } 100 | 101 | &-2 { 102 | font-size: 6em; 103 | color: silver; 104 | } 105 | 106 | &-2-avatar { 107 | width: 60px; 108 | height: 60px; 109 | font-size: 30px; 110 | line-height: 60px; 111 | } 112 | 113 | &-3 { 114 | font-size: 4em; 115 | color: #CD7F32; 116 | } 117 | 118 | &-3-avatar { 119 | width: 40px; 120 | height: 40px; 121 | font-size: 20px; 122 | line-height: 40px; 123 | } 124 | 125 | &-image { 126 | display: block; 127 | margin: 10% auto; 128 | } 129 | 130 | &-info { 131 | padding-left: 38.6%; 132 | } 133 | 134 | &-points { 135 | font-size: 50px; 136 | } 137 | 138 | span.username { 139 | display: block; 140 | } 141 | 142 | .info-avatar { 143 | display: block; 144 | margin: 0 auto 15px; 145 | position: inherit; 146 | } 147 | } 148 | 149 | @media (min-width: 992px) { 150 | .RankingPage .sideNav { 151 | padding: 0 0; 152 | white-space: nowrap; 153 | overflow: auto; 154 | -webkit-overflow-scrolling: touch; 155 | float: none; 156 | width: auto; 157 | } 158 | 159 | .RankingPage .sideNav:after { 160 | content: " "; 161 | position: absolute; 162 | left: 0; 163 | right: 0; 164 | margin-top: 15px; 165 | border-bottom: 1px solid #e8ecf3 166 | } 167 | 168 | .RankingPage .sideNav > ul > li, .RankingPage .sideNav .Dropdown-menu > li { 169 | display: inline-block; 170 | margin: 0 20px 0 0; 171 | vertical-align: top 172 | } 173 | 174 | .RankingPage .sideNav .Dropdown-separator { 175 | display: none 176 | } 177 | 178 | .RankingPage .sideNav .Dropdown--select .Dropdown-menu > li > a { 179 | padding-left: 25px 180 | } 181 | 182 | .RankingPage .sideNav .Dropdown--select .Dropdown-menu > li > a .icon { 183 | margin-left: -25px 184 | } 185 | 186 | .RankingPage .sideNav .affix { 187 | position: static 188 | } 189 | 190 | .RankingPage .sideNav:after { 191 | display: none 192 | } 193 | 194 | .RankingPage .sideNav > ul > li:first-child { 195 | width: 190px 196 | } 197 | 198 | .RankingPage .sideNavOffset { 199 | margin: 15px 0 0 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /resources/less/lib/rankLabel.less: -------------------------------------------------------------------------------- 1 | .rankLabel { 2 | font-size: 110%; 3 | font-weight: 600; 4 | display: inline-block; 5 | padding: 0.2em 1.0em; 6 | border-radius: @border-radius; 7 | background: @control-bg; 8 | color: @control-color; 9 | text-transform: none; 10 | 11 | &.colored { 12 | .rankLabel-text { 13 | color: @body-bg !important; 14 | } 15 | } 16 | } 17 | 18 | .rankLabel { 19 | 20 | .rankLabel { 21 | border-radius: 0; 22 | 23 | &:first-child { 24 | border-radius: @border-radius 0 0 @border-radius; 25 | } 26 | 27 | &:last-child { 28 | border-radius: 0 @border-radius @border-radius 0; 29 | } 30 | 31 | &:first-child:last-child { 32 | border-radius: @border-radius; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/locale/en.yml: -------------------------------------------------------------------------------- 1 | core: 2 | forum: 3 | index_sort: 4 | hot_button: Trending 5 | 6 | reflar-gamification: 7 | forum: 8 | vote_title: Show Who Voted 9 | nav: 10 | name: Rankings 11 | ranking: 12 | rank: Place 13 | name: User 14 | amount: Points 15 | notification: 16 | upvote: "{username} upvoted your post" 17 | downvote: "{username} downvoted your post" 18 | user: 19 | points: "Points: {points}" 20 | rank: "{rank}" 21 | modal: 22 | title: Voters 23 | upvotes_label: 'Upvoters:' 24 | downvotes_label: 'Downvoters:' 25 | mod_item: View Voters 26 | admin: 27 | permissions: 28 | vote_label: Upvote/Downvote posts 29 | see_votes_label: See post upvotes/downvotes 30 | see_ranking_page: See ranking page 31 | nav: 32 | desc: Gamification for your Flarum community 33 | page: 34 | rankings: 35 | blocked: 36 | placeholder: CDK2020, Ralkage, AngelAvila 37 | title: Ignored Users 38 | help: These users will not be shown on the ranking page. Usernames should be seperated by a comma followed by a space. 39 | title: Rankings Page 40 | enable: Enable custom images 41 | custom_image_1: 1st place image 42 | custom_image_2: 2nd place image 43 | custom_image_3: 3rd place image 44 | votes: 45 | points_title: Points Placeholder 46 | points_placeholder: "Points: " 47 | color_holder: '#ffffff' 48 | auto_upvote: Auto upvote posts when posted 49 | title: Votes 50 | vote_color: Voted color 51 | icon_name: Upvote/downvote icon 52 | icon_help: "Input any Font-Awesome icon that is suffixed with -up and -down. Examples: arrow, thumbs, chevron" 53 | save_settings: Save settings 54 | convert: 55 | button: Convert likes to upvotes 56 | help: Convert your previous likes from flarum-ext-likes into upvotes, as well as calculate the hotness for all current discussions. 57 | converting: Your likes are now being converted. Refresh your site after a few minutes to see the process finished. (Conversion time might take a while depending on your total forum likes) 58 | converted: "Successfully converted all {number} likes" 59 | ranks: 60 | title: Ranks 61 | ranks: Custom ranks 62 | number_title: How many rank badges should be shown? 63 | help: 64 | color: '#ffffff' 65 | help: "Input the required number of upvotes, the name of the rank, and the hex color of the rank" 66 | points: Points 67 | name: Name -------------------------------------------------------------------------------- /resources/locale/es.yml: -------------------------------------------------------------------------------- 1 | core: 2 | forum: 3 | index_sort: 4 | hot_button: Más destacado 5 | 6 | reflar-gamification: 7 | forum: 8 | vote_title: Mostrar quién ha votado 9 | nav: 10 | name: Rankings 11 | ranking: 12 | rank: Posición 13 | name: Usuario 14 | amount: Puntos 15 | notification: 16 | upvote: "{username} ha dado un voto positivo en tu publicación" 17 | downvote: "{username} ha dado un voto negativo en tu publicación" 18 | user: 19 | points: "Puntos: {points}" 20 | rank: "{rank}" 21 | modal: 22 | title: Votos 23 | upvotes_label: Votos Positivos 24 | downvotes_label: Votos Negativos 25 | admin: 26 | permissions: 27 | vote_label: Publicaciones Positivas/Negativas 28 | see_votes_label: Ver votos Positivos y Negativos de este post. 29 | see_ranking_page: Ver la página de clasificación 30 | nav: 31 | desc: Gamificación para tu comunidad Flarum. 32 | page: 33 | rankings: 34 | blocked: 35 | placeholder: CDK2020, Ralkage, AngelAvila 36 | title: Usuarios Ignorados 37 | help: Estos usuarios no aparecerán en el ranking de votos. Cada nombre de usuario debe separarse por una coma seguido de un espacio. 38 | title: Página de Rankings 39 | enable: Habilitar imágenes personalizadas. 40 | custom_image_1: Imagen 1er lugar 41 | custom_image_2: Imagen 2do lugar 42 | custom_image_3: Imagen 3er lugar 43 | votes: 44 | points_title: Marcador de puntos 45 | points_placeholder: "Puntos: " 46 | color_holder: '#ffffff' 47 | auto_upvote: Votar posts propios automáticamente cada vez que se posteen. 48 | title: Votos 49 | vote_color: Color si se ha votado. 50 | icon_name: Icono para los votos Positivos/Negativos 51 | icon_help: "Escribe el nombre de cualquier icono de Font-Awesome que tenga como sufijo -up y -down. Ejemplos: arrow, thumbs, chevron." 52 | save_settings: Guardar ajustes 53 | convert: 54 | button: Convertir likes en votos positivos. 55 | help: Convertir todos tus likes anteriores de flarum-ext-likes en votos positivos, asi como calcular la popularidad las discusiones actuales. 56 | converting: Tus likes están siendo convertidos. Espera unos minutos y actualiza esta página para ver el proceso terminado. (El tiempo de conversión depende de la cantidad de Likes en todo tu foro) 57 | converted: "Se han convertido {number} likes exitósamente" 58 | ranks: 59 | title: Rangos 60 | ranks: Rangos personalizados 61 | number_title: Cuántas insignias deberían mostrarse? 62 | help: 63 | color: '#ffffff' 64 | help: "Escribe la cantidad de votos positivos requeridos, el nombre del rango y el codigo HEX del color para el rango." 65 | points: Puntos 66 | name: Nombre 67 | -------------------------------------------------------------------------------- /resources/locale/fr.yml: -------------------------------------------------------------------------------- 1 | core: 2 | forum: 3 | index_sort: 4 | hot_button: Tendance 5 | 6 | reflar-gamification: 7 | forum: 8 | vote_title: Afficher qui a voté 9 | nav: 10 | name: Classements 11 | ranking: 12 | rank: Placer 13 | name: Utilisateur 14 | amount: Points 15 | notification: 16 | upvote: "{username} a upvoter votre message" 17 | downvote: "{username} a downvoter votre message" 18 | user: 19 | points: "Points: {points}" 20 | rank: "{rank}" 21 | modal: 22 | title: Votes 23 | upvotes_label: Upvotes 24 | downvotes_label: Downvotes 25 | admin: 26 | permissions: 27 | vote_label: Upvoter/Downvoter messages 28 | see_votes_label: Voir les upvotes/downvotes des messages 29 | see_ranking_page: Voir la page de classement 30 | nav: 31 | desc: Gamification for your Flarum community 32 | page: 33 | rankings: 34 | blocked: 35 | placeholder: CDK2020, Ralkage, AngelAvila 36 | title: Utilisateurs ignorés 37 | help: Ces utilisateurs ne seront pas affichés sur la page de classement. Les noms d'utilisateur doivent être séparés par une virgule suivie d'un espace. 38 | title: Page de classement 39 | enable: Activer les images personnalisées 40 | custom_image_1: 1ère place image 41 | custom_image_2: 2ème place image 42 | custom_image_3: 3ème place image 43 | votes: 44 | points_title: Points Placeholder 45 | points_placeholder: "Points: " 46 | color_holder: '#ffffff' 47 | auto_upvote: AutoUpvote les messages lors de la publication 48 | title: Votes 49 | vote_color: Couleur votée 50 | icon_name: Upvote/downvote icon 51 | icon_help: "Entrez n'importe quelle icône Font-Awesome qui est suffixée avec -up and -down. Examples: arrow, thumbs, chevron" 52 | save_settings: Enregistrer les paramètres 53 | convert: 54 | button: Convertir les likes en upvotes 55 | help: Convertissez vos paramètres antérieurs de Flarum-ext-likes en upvotes, et calculez le classements pour toutes les discussions en cours. 56 | converting: Vos paramètres sont en cours de conversion. Actualisez votre site après quelques minutes pour voir le processus terminé. (Le temps de conversion peut prendre un certain temps en fonction de vos préférences de forum) 57 | converted: "Transformé avec succès tous {number} votes" 58 | ranks: 59 | title: Classements 60 | ranks: Classements personnalisés 61 | number_title: Combien de badges de rang devraient être affichés? 62 | help: 63 | color: '#ffffff' 64 | help: "Entrez le nombre requis de votes upvotes, le nom du rang et la couleur hexadécimale du rang" 65 | points: Points 66 | name: Nom 67 | -------------------------------------------------------------------------------- /resources/locale/it.yml: -------------------------------------------------------------------------------- 1 | core: 2 | forum: 3 | index_sort: 4 | hot_button: Tendenze 5 | 6 | reflar-gamification: 7 | forum: 8 | vote_title: Mostra Chi Ha Votato 9 | nav: 10 | name: Classifiche 11 | ranking: 12 | rank: Posizione 13 | name: Utente 14 | amount: Punteggio 15 | notification: 16 | upvote: "{username} ha votato positivamente il tuo messaggio" 17 | downvote: "{username} ha votato negativamente il tuo messaggio" 18 | user: 19 | points: "Punteggio: {points}" 20 | rank: "{rank}" 21 | modal: 22 | title: Votanti 23 | upvotes_label: 'Positivi:' 24 | downvotes_label: 'Negativi:' 25 | mod_item: Visualizza Votanti 26 | admin: 27 | permissions: 28 | vote_label: Vota positivamente/negativamente i messaggi 29 | see_votes_label: Visualizza i voti positivi/negativi 30 | see_ranking_page: Visualizza la classifica 31 | nav: 32 | desc: Ludicizzazione per la tua comunità Flarum 33 | page: 34 | rankings: 35 | blocked: 36 | placeholder: CDK2020, Ralkage, AngelAvila 37 | title: Utenti Ignorati 38 | help: Questi utenti non saranno mostrati nella classificha. I nomi utente devono essere separati da una virgola seguita da uno spazio. 39 | title: Classifica 40 | enable: Abilita immagini personalizzate 41 | custom_image_1: 1ª posizione 42 | custom_image_2: 2ª posizione 43 | custom_image_3: 3ª posizione 44 | votes: 45 | points_title: Segnaposto Punteggio 46 | points_placeholder: "Punteggio: " 47 | color_holder: '#ffffff' 48 | auto_upvote: Vota in automatico e positivamente i messaggi quando pubblicati 49 | title: Voti 50 | vote_color: Colore votato 51 | icon_name: Icona voto positivo/negativo 52 | icon_help: "Inserisci qualsiasi icona Font-Awesome che termina con -up e -down. Esempi: arrow, thumbs, chevron" 53 | save_settings: Salva impostazioni 54 | convert: 55 | button: Converti Mi Piace in voti positivi 56 | help: Converti i tuoi Mi Piace precedenti di flarum-ext-likes in voti positivi, così come calcola il gradimento per tutte le discussioni correnti. 57 | converting: I tuoi Mi Piace sono in conversione. Ricarica il tuo sito tra qualche minuto per vedere i risultati. (I tempi di conversione potrebbero durare un po' a seconda del numero totale dei Mi Piace nel tuo forum) 58 | converted: "Conversione con successo di {number} Mi Piace" 59 | ranks: 60 | title: Posizioni 61 | ranks: Posizioni Personalizzate 62 | number_title: Quanti distintivi di posizione devono essere mostrati? 63 | help: 64 | color: '#ffffff' 65 | help: "Inserisci il numero richiesto di voti positivi, il nome della posizione, e il colore in esadecimale della posizione" 66 | points: Punteggio 67 | name: Nome 68 | -------------------------------------------------------------------------------- /resources/locale/pl.yml: -------------------------------------------------------------------------------- 1 | core: 2 | forum: 3 | index_sort: 4 | hot_button: Popularne 5 | 6 | reflar-gamification: 7 | forum: 8 | vote_title: Pokaż, kto zagłosował 9 | nav: 10 | name: Rankingi 11 | ranking: 12 | rank: Miejsce 13 | name: Użytkownik 14 | amount: Punkty 15 | notification: 16 | upvote: "{username} oddał głos w górę na twój post" 17 | downvote: "{username} oddał głos w dół na twój post" 18 | user: 19 | points: "Punkty: {points}" 20 | rank: "{rank}" 21 | modal: 22 | title: Głosujący 23 | upvotes_label: 'Upvoters:' 24 | downvotes_label: 'Downvoters:' 25 | mod_item: Zobacz Głosujących 26 | admin: 27 | permissions: 28 | vote_label: Posty w górę/dół 29 | see_votes_label: Zobacz posty w górę/dół 30 | see_ranking_page: Zobacz stronę rankingową 31 | nav: 32 | desc: Gamification dla twojej społeczności Flarum 33 | page: 34 | rankings: 35 | blocked: 36 | placeholder: CDK2020, Ralkage, AngelAvila 37 | title: Zignorowani użytkownicy 38 | help: Ci użytkownicy nie będą wyświetlani na stronie rankingu. Nazwy użytkowników powinny być oddzielone przecinkiem i spacją. 39 | title: Rankingi 40 | enable: Włącz niestandardowe zdjęcia 41 | custom_image_1: Zdjęcie 1 42 | custom_image_2: Zdjęcie 2 43 | custom_image_3: Zdjęcie 3 44 | votes: 45 | points_title: Punkty Placeholder 46 | points_placeholder: "Punkty: " 47 | color_holder: '#ffffff' 48 | auto_upvote: Automatycznie śledź posty po ich opublikowaniu 49 | title: Głosy 50 | vote_color: Głosowany kolor 51 | icon_name: Ikonka głosów w górę/dół 52 | icon_help: "Wprowadź dowolną ikonę Font-Awesome. Przykłady: arrow, thumbs, chevron" 53 | save_settings: Zapisz ustawienia 54 | convert: 55 | button: Konwertuj polubienia na głosy w górę 56 | help: Convert your previous likes from flarum-ext-likes into upvotes, as well as calculate the hotness for all current discussions. 57 | converting: Your likes are now being converted. Refresh your site after a few minutes to see the process finished. (Conversion time might take a while depending on your total forum likes) 58 | converted: "Pomyślnie skonwertowano wszystkie {number} polubień" 59 | ranks: 60 | title: Rangi 61 | ranks: Rangi niestandardowe 62 | number_title: Ile rang należy pokazywać? 63 | help: 64 | color: '#ffffff' 65 | help: "Wprowadź wymaganą liczbę Głosów w górę, nazwę rangi oraz jej kolor" 66 | points: Punkty 67 | name: Nazwa 68 | -------------------------------------------------------------------------------- /resources/locale/pt-br.yml: -------------------------------------------------------------------------------- 1 | core: 2 | forum: 3 | index_sort: 4 | hot_button: Tendências 5 | 6 | reflar-gamification: 7 | forum: 8 | vote_title: Mostrar quem votou 9 | nav: 10 | name: Classificação 11 | ranking: 12 | rank: Posição 13 | name: Usuário 14 | amount: Pontos 15 | notification: 16 | upvote: "{username} deu voto positivo na sua publicação" 17 | downvote: "{username} de voto negativo na sua publicação" 18 | user: 19 | points: "Pontos: {points}" 20 | rank: "{rank}" 21 | modal: 22 | title: Votos 23 | upvotes_label: 'Votos Positivos:' 24 | downvotes_label: 'Votos Negativos:' 25 | mod_item: Ver Votos 26 | admin: 27 | permissions: 28 | vote_label: Positivar/negativar publicações 29 | see_votes_label: Ver votos positivos e negativos da publicação 30 | see_ranking_page: Ver classificação 31 | nav: 32 | desc: Gamificação para sua comunidade Flarum 33 | page: 34 | rankings: 35 | blocked: 36 | placeholder: CDK2020, Ralkage, AngelAvila 37 | title: Usuários ignorados 38 | help: Estes usuários não serão mostrados na página de classificação. Cada nome de usuário deve ser separado por uma vígula seguida de um espaço. 39 | title: Página de classificação 40 | enable: Habilitar imagens personalizadas 41 | custom_image_1: Imagem do 1º lugar 42 | custom_image_2: Imagem do 2º lugar 43 | custom_image_3: Imagem do 3º lugar 44 | votes: 45 | points_title: Marcador de pontos 46 | points_placeholder: "Pontos: " 47 | color_holder: '#ffffff' 48 | auto_upvote: Positivar automaticamente publicações quando postadas 49 | title: Votos 50 | vote_color: Cor do ícone após votar 51 | icon_name: Ícone dos votos 52 | icon_help: "Insira qualquer ícone do Font-Awesome com sufixo -up e -down. Exemplos: arrow, thumbs, chevron" 53 | save_settings: Salvar configurações 54 | convert: 55 | button: Converter curtidas para votos positivos 56 | help: Converta suas curtidas da extensão flarum-ext-likes em votos positivos, além de calcular a popularidade para todas as discussões atuais. 57 | converting: Suas curtidas estão agora sendo convertidas. Recarregue seu site após alguns minutos para ver o processo finalizado. (O tempo de conversão talvez demore dependendo do seu total de curtidas) 58 | converted: "Foram convertidas {number} curtidas com sucesso" 59 | ranks: 60 | title: Classificações 61 | ranks: Classificações personalizadas 62 | number_title: Quantos badges de classificação devem ser mostrados? 63 | help: 64 | color: '#ffffff' 65 | help: "Insira o número de votos positivos, o nome, e a cor hexadecimal do badge" 66 | points: Pontos 67 | name: Nome 68 | -------------------------------------------------------------------------------- /scripts/compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script compiles the extension so that it can be used in a Flarum 4 | # installation. It should be run from the root directory of the extension. 5 | 6 | base=$PWD 7 | 8 | cd "${base}/js" 9 | 10 | if [ -f bower.json ]; then 11 | bower install 12 | fi 13 | 14 | for app in forum admin; do 15 | cd "${base}/js" 16 | 17 | if [ -d $app ]; then 18 | cd $app 19 | 20 | if [ -f bower.json ]; then 21 | bower install 22 | fi 23 | 24 | npm install 25 | gulp --production 26 | fi 27 | done 28 | -------------------------------------------------------------------------------- /scripts/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | base=$PWD 4 | 5 | git add . 6 | 7 | cd scripts 8 | 9 | php PreCommit.php 10 | 11 | if [ $? -eq 1 ]; 12 | then 13 | exit 14 | else 15 | cd ../ 16 | 17 | read -p "Commit message: " commit 18 | git commit -m "$commit" 19 | 20 | read -p "Would you like to push? [y/n]: " yn 21 | 22 | if [ $yn = "y" ]; 23 | then 24 | git push 25 | else 26 | exit 27 | fi 28 | fi 29 | -------------------------------------------------------------------------------- /src/Access/DiscussionPolicy.php: -------------------------------------------------------------------------------- 1 | is_locked && $actor->cannot('lock', $discussion)) { 35 | return false; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Api/Controllers/ConvertLikesController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 46 | $this->gamification = $gamification; 47 | } 48 | 49 | /** 50 | * @param ServerRequestInterface $request 51 | * 52 | * @return int 53 | */ 54 | public function handle(ServerRequestInterface $request): ResponseInterface 55 | { 56 | $actor = $request->getAttribute('actor'); 57 | 58 | if (null !== $actor && $actor->isAdmin() && 'POST' === $request->getMethod() && false == $this->settings->get('reflar.gamification.convertedLikes')) { 59 | $likes = Likes::all(); 60 | 61 | $this->settings->set('reflar.gamification.convertedLikes', 'converting'); 62 | 63 | $counter = 0; 64 | 65 | foreach ($likes as $like) { 66 | $this->gamification->convertLike($like->post_id, $like->user_id); 67 | $counter++; 68 | } 69 | 70 | $discussions = Discussion::all(); 71 | 72 | foreach ($discussions as $discussion) { 73 | $this->gamification->calculateHotness($discussion); 74 | } 75 | 76 | $this->settings->set('reflar.gamification.convertedLikes', $counter); 77 | 78 | return new JsonResponse($counter, 200); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Api/Controllers/CreateRankController.php: -------------------------------------------------------------------------------- 1 | bus = $bus; 37 | } 38 | 39 | /** 40 | * @param ServerRequestInterface $request 41 | * @param Document $document 42 | * 43 | * @return mixed 44 | */ 45 | protected function data(ServerRequestInterface $request, Document $document) 46 | { 47 | return $this->bus->dispatch( 48 | new CreateRank($request->getAttribute('actor'), array_get($request->getParsedBody(), 'data', [])) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Api/Controllers/DeleteRankController.php: -------------------------------------------------------------------------------- 1 | bus = $bus; 33 | } 34 | 35 | /** 36 | * @param ServerRequestInterface $request 37 | */ 38 | protected function delete(ServerRequestInterface $request) 39 | { 40 | $this->bus->dispatch( 41 | new DeleteRank(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Api/Controllers/DeleteTopImageController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 45 | $this->app = $app; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function delete(ServerRequestInterface $request) 52 | { 53 | $id = array_get($request->getQueryParams(), 'id'); 54 | 55 | $this->assertAdmin($request->getAttribute('actor')); 56 | 57 | $path = $this->settings->get('topimage'.$id.'_path'); 58 | 59 | $this->settings->set('topimage'.$id.'_path', null); 60 | 61 | $uploadDir = new Filesystem(new Local($this->app->publicPath().'/assets')); 62 | 63 | if ($uploadDir->has($path)) { 64 | $uploadDir->delete($path); 65 | } 66 | 67 | return new EmptyResponse(204); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Api/Controllers/ListRanksController.php: -------------------------------------------------------------------------------- 1 | gamification = $gamification; 42 | } 43 | 44 | /** 45 | * @param ServerRequestInterface $request 46 | * 47 | * @return mixed 48 | */ 49 | protected function data(ServerRequestInterface $request, Document $document) 50 | { 51 | if ($request->getAttribute('actor')->cannot('reflar.gamification.viewRankingPage')) { 52 | throw new PermissionDeniedException(); 53 | } 54 | 55 | $limit = $this->extractLimit($request); 56 | $offset = $this->extractOffset($request); 57 | 58 | return $this->gamification->orderByPoints($limit, $offset); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Api/Controllers/UpdateRankController.php: -------------------------------------------------------------------------------- 1 | bus = $bus; 37 | } 38 | 39 | /** 40 | * @param ServerRequestInterface $request 41 | * @param Document $document 42 | * 43 | * @return mixed 44 | */ 45 | protected function data(ServerRequestInterface $request, Document $document) 46 | { 47 | return $this->bus->dispatch( 48 | new EditRank(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'), array_get($request->getParsedBody(), 'data', [])) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Api/Controllers/UploadTopImageController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 47 | $this->app = $app; 48 | } 49 | 50 | public function data(ServerRequestInterface $request, Document $document) 51 | { 52 | $this->assertAdmin($request->getAttribute('actor')); 53 | 54 | $id = array_get($request->getQueryParams(), 'id'); 55 | 56 | $file = array_get($request->getUploadedFiles(), 'topimage'.$id); 57 | 58 | $tmpFile = tempnam($this->app->storagePath().'/tmp', 'topimage.'.$id); 59 | $file->moveTo($tmpFile); 60 | 61 | $extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION); 62 | 63 | if ('1' == $id) { 64 | $size = 125; 65 | } elseif ('2' == $id) { 66 | $size = 75; 67 | } else { 68 | $size = 50; 69 | } 70 | 71 | $manager = new ImageManager(); 72 | 73 | $encodedImage = $manager->make($tmpFile)->resize($size, $size)->encode('png'); 74 | file_put_contents($tmpFile, $encodedImage); 75 | 76 | $extension = 'png'; 77 | 78 | $mount = new MountManager([ 79 | 'source' => new Filesystem(new Local(pathinfo($tmpFile, PATHINFO_DIRNAME))), 80 | 'target' => new Filesystem(new Local($this->app->publicPath().'/assets')), 81 | ]); 82 | 83 | if (($path = $this->settings->get('topimage'.$id.'_path')) && $mount->has($file = "target://$path")) { 84 | $mount->delete($file); 85 | } 86 | 87 | $uploadName = 'topimage-'.Str::lower(Str::random(8)).'.'.$extension; 88 | 89 | $mount->move('source://'.pathinfo($tmpFile, PATHINFO_BASENAME), "target://$uploadName"); 90 | 91 | $this->settings->set('topimage'.$id.'_path', $uploadName); 92 | 93 | return parent::data($request, $document); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Api/Serializers/RankSerializer.php: -------------------------------------------------------------------------------- 1 | $rank->points, 40 | 'name' => $rank->name, 41 | 'color' => $rank->color, 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Commands/CreateRank.php: -------------------------------------------------------------------------------- 1 | actor = $actor; 36 | $this->data = $data; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/CreateRankHandler.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 35 | } 36 | 37 | /** 38 | * @param CreateRank $command 39 | * 40 | * @throws PermissionDeniedException 41 | * 42 | * @return Rank 43 | */ 44 | public function handle(CreateRank $command) 45 | { 46 | $actor = $command->actor; 47 | $data = $command->data; 48 | 49 | $this->assertAdmin($actor); 50 | 51 | $rank = Rank::build( 52 | array_get($data, 'attributes.name'), 53 | array_get($data, 'attributes.color'), 54 | array_get($data, 'attributes.points') 55 | ); 56 | 57 | $this->validator->assertValid($rank->getAttributes()); 58 | 59 | $rank->save(); 60 | 61 | return $rank; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Commands/DeleteRank.php: -------------------------------------------------------------------------------- 1 | rankId = $rankId; 35 | $this->actor = $actor; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Commands/DeleteRankHandler.php: -------------------------------------------------------------------------------- 1 | actor; 33 | 34 | $this->assertAdmin($actor); 35 | 36 | $rank = Rank::where('id', $command->rankId)->firstOrFail(); 37 | 38 | $rank->delete(); 39 | 40 | return $rank; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Commands/EditRank.php: -------------------------------------------------------------------------------- 1 | rankId = $rankId; 42 | $this->actor = $actor; 43 | $this->data = $data; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Commands/EditRankHandler.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 35 | } 36 | 37 | /** 38 | * @param EditRank $command 39 | * 40 | * @throws PermissionDeniedException 41 | * 42 | * @return Rank 43 | */ 44 | public function handle(EditRank $command) 45 | { 46 | $actor = $command->actor; 47 | $data = $command->data; 48 | $attributes = array_get($data, 'attributes', []); 49 | 50 | $validate = []; 51 | 52 | $this->assertAdmin($actor); 53 | 54 | $rank = Rank::where('id', $command->rankId)->firstOrFail(); 55 | 56 | if (isset($attributes['points']) && '' !== $attributes['points']) { 57 | $validate['points'] = $attributes['points']; 58 | $rank->updatePoints($attributes['points']); 59 | } 60 | 61 | if (isset($attributes['name']) && '' !== $attributes['name']) { 62 | $validate['name'] = $attributes['name']; 63 | $rank->updateName($attributes['name']); 64 | } 65 | 66 | if (isset($attributes['color']) && '' !== $attributes['color']) { 67 | $validate['color'] = $attributes['color']; 68 | $rank->updateColor($attributes['color']); 69 | } 70 | 71 | $this->validator->assertValid(array_merge($rank->getDirty(), $validate)); 72 | 73 | $rank->save(); 74 | 75 | return $rank; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Events/PostWasVoted.php: -------------------------------------------------------------------------------- 1 | post = $post; 51 | $this->user = $user; 52 | $this->actor = $actor; 53 | $this->type = $type; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Gambit/HotGambit.php: -------------------------------------------------------------------------------- 1 | getQuery()->orderBy('hotness', 'desc'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Gamification.php: -------------------------------------------------------------------------------- 1 | posts = $posts; 50 | $this->users = $users; 51 | $this->settings = $settings; 52 | } 53 | 54 | /** 55 | * The Reddit hotness algorithm from https://github.com/reddit/reddit. 56 | * 57 | * @param $discussion 58 | */ 59 | public function calculateHotness($discussion) 60 | { 61 | $date = strtotime($discussion->start_time); 62 | 63 | $s = $discussion->votes; 64 | $order = log10(max(abs($s), 1)); 65 | 66 | if ($s > 0) { 67 | $sign = 1; 68 | } elseif ($s < 0) { 69 | $sign = -1; 70 | } else { 71 | $sign = 0; 72 | } 73 | 74 | $seconds = $date - 1134028003; 75 | 76 | $discussion->hotness = round($sign * $order + $seconds / 45000, 10); 77 | 78 | $discussion->save(); 79 | } 80 | 81 | /** 82 | * @return mixed 83 | */ 84 | public function orderByPoints($limit, $offset) 85 | { 86 | $blockedUsers = explode(', ', $this->settings->get('reflar.gamification.blockedUsers')); 87 | 88 | if ($limit > self::MAXIMUM_USER_EXPOSED) { 89 | $limit = self::MAXIMUM_USER_EXPOSED; 90 | } 91 | 92 | if ($offset >= self::MAXIMUM_USER_EXPOSED) { 93 | return []; 94 | } 95 | 96 | if (($limit + $offset) > self::MAXIMUM_USER_EXPOSED) { 97 | $limit = $limit + $offset - self::MAXIMUM_USER_EXPOSED; 98 | } 99 | 100 | $query = User::query() 101 | ->whereNotIn('username', $blockedUsers) 102 | ->orderBy('votes', 'desc') 103 | ->offset($offset) 104 | ->take($limit) 105 | ->get(); 106 | 107 | return $query; 108 | } 109 | 110 | /** 111 | * @param $post_id 112 | * @param $user_id 113 | * @param User $actor 114 | */ 115 | public function convertLike($post_id, $user_id) 116 | { 117 | $user = $this->users->query()->where('id', $user_id)->first(); 118 | $post = $this->posts->query()->where('id', $post_id)->first(); 119 | 120 | if (null !== $post && null !== $post->user && null !== $user) { 121 | $post->user->increment('votes'); 122 | 123 | if ($post->number = 1) { 124 | $post->discussion->increment('votes'); 125 | } 126 | 127 | $vote = vote::build($post, $user); 128 | $vote->type = 'Up'; 129 | $vote->save(); 130 | 131 | $ranks = json_decode($this->settings->get('reflar.gamification.ranks'), true); 132 | 133 | if (isset($ranks[$post->user->votes])) { 134 | $post->user->rank = $ranks[$post->user->votes]; 135 | $post->user->save(); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Likes.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 40 | } 41 | 42 | /** 43 | * @param Dispatcher $events 44 | */ 45 | public function subscribe(Dispatcher $events) 46 | { 47 | $events->listen(GetModelRelationship::class, [$this, 'getModelRelationship']); 48 | $events->listen(WillSerializeData::class, [$this, 'loadRanksRelationship']); 49 | $events->listen(GetApiRelationship::class, [$this, 'getApiAttributes']); 50 | $events->listen(Serializing::class, [$this, 'prepareApiAttributes']); 51 | $events->listen(WillGetData::class, [$this, 'includeLikes']); 52 | } 53 | 54 | /** 55 | * @param GetModelRelationship $event 56 | * 57 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany|null 58 | */ 59 | public function getModelRelationship(GetModelRelationship $event) 60 | { 61 | if ($event->isRelationship(Post::class, 'upvotes')) { 62 | return $event->model->belongsToMany(User::class, 'post_votes', 'post_id', 'user_id', null, null, 'upvotes')->where('type', 'Up'); 63 | } 64 | 65 | if ($event->isRelationship(Post::class, 'downvotes')) { 66 | return $event->model->belongsToMany(User::class, 'post_votes', 'post_id', 'user_id', null, null, 'downvotes')->where('type', 'Down'); 67 | } 68 | 69 | if ($event->isRelationship(User::class, 'ranks')) { 70 | return $event->model->belongsToMany(Rank::class, 'rank_user', null, null, null, null, 'ranks'); 71 | } 72 | } 73 | 74 | /** 75 | * @param GetApiRelationship $event 76 | * 77 | * @return \Tobscure\JsonApi\Relationship|null 78 | */ 79 | public function getApiAttributes(GetApiRelationship $event) 80 | { 81 | if ($event->isRelationship(Serializer\PostSerializer::class, 'upvotes')) { 82 | return $event->serializer->hasMany($event->model, Serializer\BasicUserSerializer::class, 'upvotes'); 83 | } 84 | 85 | if ($event->isRelationship(Serializer\PostSerializer::class, 'downvotes')) { 86 | return $event->serializer->hasMany($event->model, Serializer\BasicUserSerializer::class, 'downvotes'); 87 | } 88 | 89 | if ($event->isRelationship(Serializer\ForumSerializer::class, 'ranks') || $event->isRelationship(Serializer\UserSerializer::class, 'ranks')) { 90 | return $event->serializer->hasMany($event->model, RankSerializer::class, 'ranks'); 91 | } 92 | } 93 | 94 | /** 95 | * @param WillSerializeData $event 96 | */ 97 | public function loadRanksRelationship(WillSerializeData $event) 98 | { 99 | if ($event->isController(Controller\ShowForumController::class)) { 100 | $event->data['ranks'] = Rank::get(); 101 | } 102 | } 103 | 104 | /** 105 | * @param Serializing $event 106 | */ 107 | public function prepareApiAttributes(Serializing $event) 108 | { 109 | if ($event->isSerializer(Serializer\UserSerializer::class)) { 110 | $event->attributes['canViewRankingPage'] = (bool) $event->actor->can('reflar.gamification.viewRankingPage'); 111 | $event->attributes['Points'] = $event->model->votes; 112 | } 113 | if ($event->isSerializer(Serializer\ForumSerializer::class)) { 114 | $event->attributes['IconName'] = $this->settings->get('reflar.gamification.iconName'); 115 | $event->attributes['PointsPlaceholder'] = $this->settings->get('reflar.gamification.pointsPlaceholder'); 116 | $event->attributes['DefaultLocale'] = $this->settings->get('default_locale'); 117 | $event->attributes['CustomRankingImages'] = $this->settings->get('reflar.gamification.customRankingImages'); 118 | $event->attributes['topimage1Url'] = "/assets/{$this->settings->get('topimage1_path')}"; 119 | $event->attributes['topimage2Url'] = "/assets/{$this->settings->get('topimage2_path')}"; 120 | $event->attributes['topimage3Url'] = "/assets/{$this->settings->get('topimage3_path')}"; 121 | $event->attributes['ranksAmt'] = $this->settings->get('reflar.gamification.rankAmt'); 122 | } 123 | if ($event->isSerializer(Serializer\DiscussionSerializer::class)) { 124 | $event->attributes['votes'] = (int) $event->model->votes; 125 | $event->attributes['canVote'] = (bool) $event->actor->can('vote', $event->model); 126 | $event->attributes['canSeeVotes'] = (bool) $event->actor->can('canSeeVotes', $event->model); 127 | } 128 | } 129 | 130 | /** 131 | * @param WillGetData $event 132 | */ 133 | public function includeLikes(WillGetData $event) 134 | { 135 | if ($event->isController(Controller\ListUsersController::class) 136 | || $event->isController(Controller\ShowUserController::class) 137 | || $event->isController(Controller\CreateUserController::class) 138 | || $event->isController(OrderByPointsController::class) 139 | || $event->isController(Controller\UpdateUserController::class)) { 140 | $event->addInclude('ranks'); 141 | } 142 | if ($event->isController(Controller\ShowDiscussionController::class)) { 143 | $event->addInclude(['posts.upvotes', 'posts.downvotes', 'posts.user.ranks']); 144 | } 145 | if ($event->isController(Controller\ListPostsController::class) 146 | || $event->isController(Controller\ShowPostController::class) 147 | || $event->isController(Controller\CreatePostController::class) 148 | || $event->isController(Controller\UpdatePostController::class)) { 149 | $event->addInclude(['upvotes', 'downvotes', 'user.ranks']); 150 | } 151 | if ($event->isController(Controller\ShowForumController::class)) { 152 | $event->addInclude('ranks'); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Listeners/EventHandlers.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 59 | $this->notifications = $notifications; 60 | $this->gamification = $gamification; 61 | } 62 | 63 | /** 64 | * @param Dispatcher $events 65 | */ 66 | public function subscribe(Dispatcher $events) 67 | { 68 | $events->listen(ConfigureNotificationTypes::class, [$this, 'registerNotificationType']); 69 | $events->listen(Posted::class, [$this, 'addVote']); 70 | $events->listen(Deleted::class, [$this, 'removeVote']); 71 | } 72 | 73 | /** 74 | * @param Posted $event 75 | */ 76 | public function addVote(Posted $event) 77 | { 78 | if ('0' !== $this->settings->get('reflar.gamification.autoUpvotePosts')) { 79 | $actor = $event->actor; 80 | 81 | $actor->increment('votes'); 82 | $event->post->discussion->increment('votes'); 83 | $this->gamification->calculateHotness($event->post->discussion); 84 | 85 | $vote = Vote::build($event->post, $actor); 86 | $vote->type = 'Up'; 87 | $vote->save(); 88 | 89 | $ranks = Rank::where('points', '<=', $actor->votes)->get(); 90 | 91 | if (null !== $ranks) { 92 | $actor->ranks()->detach(); 93 | foreach ($ranks as $rank) { 94 | $actor->ranks()->attach($rank->id); 95 | } 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * @param ConfigureNotificationTypes $event 102 | */ 103 | public function registerNotificationType(ConfigureNotificationTypes $event) 104 | { 105 | $event->add(VoteBlueprint::class, BasicPostSerializer::class, ['alert']); 106 | } 107 | 108 | /** 109 | * @param Deleted $event 110 | */ 111 | public function removeVote(Deleted $event) 112 | { 113 | $post = $event->post; 114 | 115 | foreach ($post->upvotes() as $upvote) { 116 | $upvote->delete(); 117 | } 118 | foreach ($post->downvotes() as $downvote) { 119 | $downvote->delete(); 120 | } 121 | 122 | $voteNumber = $post->upvotes()->count() - $post->downvotes()->count(); 123 | $user = $event->post->user; 124 | $user->votes = $user->votes - $voteNumber; 125 | 126 | $ranks = Rank::whereBetween('points', [$user->votes + 1, $user->votes + 2])->get(); 127 | 128 | if (null !== $ranks) { 129 | foreach ($ranks as $rank) { 130 | $user->ranks()->detach($rank->id); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Listeners/FilterDiscussionListByHotness.php: -------------------------------------------------------------------------------- 1 | listen(ConfigureDiscussionGambits::class, [$this, 'ConfigureDiscussionGambits']); 27 | } 28 | 29 | /** 30 | * @param ConfigureDiscussionGambits $event 31 | */ 32 | public function ConfigureDiscussionGambits(ConfigureDiscussionGambits $event) 33 | { 34 | $event->gambits->add(HotGambit::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Listeners/SaveVotesToDatabase.php: -------------------------------------------------------------------------------- 1 | events = $events; 63 | $this->notifications = $notifications; 64 | $this->gamification = $gamification; 65 | $this->settings = $settings; 66 | } 67 | 68 | /** 69 | * @param Dispatcher $events 70 | */ 71 | public function subscribe(Dispatcher $events) 72 | { 73 | $events->listen(Saving::class, [$this, 'whenSaving']); 74 | $events->listen(Deleted::class, [$this, 'whenDeleted']); 75 | } 76 | 77 | /** 78 | * @param Saving $event 79 | * 80 | * @throws FloodingException 81 | * @throws \Flarum\User\Exception\PermissionDeniedException 82 | */ 83 | public function whenSaving(Saving $event) 84 | { 85 | $post = $event->post; 86 | if ($post->id) { 87 | $data = $event->data; 88 | 89 | if (array_key_exists(2, $data['attributes'])) { 90 | $actor = $event->actor; 91 | $user = $post->user; 92 | 93 | $this->assertCan($actor, 'vote', $post->discussion); 94 | $this->assertNotFlooding($actor); 95 | 96 | $isUpvoted = $data['attributes'][0]; 97 | 98 | $isDownvoted = $data['attributes'][1]; 99 | 100 | $this->vote($post, $isDownvoted, $isUpvoted, $actor, $user); 101 | } 102 | } 103 | } 104 | 105 | public function vote($post, $isDownvoted, $isUpvoted, $actor, $user) 106 | { 107 | $vote = Vote::where([ 108 | 'post_id' => $post->id, 109 | 'user_id' => $actor->id, 110 | ])->first(); 111 | 112 | if ($vote) { 113 | if (!$isUpvoted && !$isDownvoted) { 114 | if ('Up' == $vote->type) { 115 | $this->changePoints($user, $post, -1); 116 | $this->pushNewVote('up2none', $post, 'up', $actor); 117 | } else { 118 | $this->changePoints($user, $post, 1); 119 | $this->pushNewVote('down2none', $post, 'down', $actor); 120 | } 121 | $this->sendData($post, $user, $actor, 'None', $vote->type); 122 | $vote->delete(); 123 | } else { 124 | if ('Up' == $vote->type) { 125 | $vote->type = 'Down'; 126 | $this->changePoints($user, $post, -2); 127 | $this->pushNewVote('up2down', $post, 'down', $actor); 128 | 129 | $this->sendData($post, $user, $actor, 'Down', 'Up'); 130 | } else { 131 | $vote->type = 'Up'; 132 | $this->changePoints($user, $post, 2); 133 | $this->pushNewVote('down2up', $post, 'up', $actor); 134 | 135 | $this->sendData($post, $user, $actor, 'Up', 'Down'); 136 | } 137 | $vote->save(); 138 | } 139 | } else { 140 | $vote = Vote::build($post, $actor); 141 | if ($isDownvoted) { 142 | $vote->type = 'Down'; 143 | $this->changePoints($user, $post, -1); 144 | $this->pushNewVote('none2down', $post, 'down', $actor); 145 | } elseif ($isUpvoted) { 146 | $vote->type = 'Up'; 147 | $this->changePoints($user, $post, 1); 148 | $this->pushNewVote('none2up', $post, 'up', $actor); 149 | } 150 | $this->sendData($post, $user, $actor, $vote->type, ' '); 151 | $vote->save(); 152 | } 153 | $actor->last_vote_time = new DateTime(); 154 | $actor->save(); 155 | } 156 | 157 | /** 158 | * @param $user 159 | * @param $post 160 | * @param $number 161 | */ 162 | public function changePoints($user, $post, $number) 163 | { 164 | $user->votes = $user->votes + $number; 165 | $discussion = $post->discussion; 166 | 167 | if (1 == $post->number) { 168 | $discussion->votes = $discussion->votes + $number; 169 | $discussion->save(); 170 | $this->gamification->calculateHotness($discussion); 171 | } 172 | $post->save(); 173 | $user->save(); 174 | } 175 | 176 | /** 177 | * @param $post 178 | * @param $user 179 | * @param $actor 180 | * @param $type 181 | */ 182 | public function sendData($post, $user, $actor, $type, $before) 183 | { 184 | $oldVote = Notification::where([ 185 | 'from_user_id' => $actor->id, 186 | 'subject_id' => $post->id, 187 | 'data' => '"'.$before.'"', 188 | ])->first(); 189 | 190 | if ($oldVote) { 191 | if ('None' === $type) { 192 | $oldVote->delete(); 193 | } else { 194 | $oldVote->data = $type; 195 | $oldVote->save(); 196 | } 197 | } elseif ($user->id !== $actor->id) { 198 | $this->notifications->sync( 199 | new VoteBlueprint($post, $actor, $type), 200 | [$user]); 201 | } 202 | 203 | $this->events->fire( 204 | new PostWasVoted($post, $user, $actor, $type) 205 | ); 206 | 207 | if ('Up' === $type) { 208 | $ranks = Rank::where('points', '<=', $user->votes)->get(); 209 | 210 | if (null !== $ranks) { 211 | $user->ranks()->detach(); 212 | foreach ($ranks as $rank) { 213 | $user->ranks()->attach($rank->id); 214 | } 215 | } 216 | } elseif ('Down' === $type) { 217 | $ranks = Rank::whereBetween('points', [$user->votes + 1, $user->votes + 2])->get(); 218 | 219 | if (null !== $ranks) { 220 | foreach ($ranks as $rank) { 221 | $user->ranks()->detach($rank->id); 222 | } 223 | } 224 | } 225 | } 226 | 227 | public function pushNewVote($type, $post, $clicked, $actor) 228 | { 229 | $type = explode('2', $type); 230 | 231 | $pusher = $this->getPusher(); 232 | $pusher->trigger('public', 'newVote', [ 233 | 'postId' => $post->id, 234 | 'before' => $type[0], 235 | 'after' => $type[1], 236 | 'clicked' => $clicked, 237 | 'userId' => $actor->id, 238 | ]); 239 | } 240 | 241 | /** 242 | * @param $user 243 | * 244 | * @throws FloodingException 245 | */ 246 | public function assertNotFlooding($actor) 247 | { 248 | if (new DateTime($actor->last_vote_time) >= new DateTime('-10 seconds')) { 249 | throw new FloodingException(); 250 | } 251 | } 252 | 253 | /** 254 | * @return Pusher 255 | */ 256 | protected function getPusher() 257 | { 258 | $options = []; 259 | if ($cluster = $this->settings->get('flarum-pusher.app_cluster')) { 260 | $options['cluster'] = $cluster; 261 | } 262 | 263 | return new Pusher( 264 | $this->settings->get('flarum-pusher.app_key'), 265 | $this->settings->get('flarum-pusher.app_secret'), 266 | $this->settings->get('flarum-pusher.app_id'), 267 | $options 268 | ); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/Notification/VoteBlueprint.php: -------------------------------------------------------------------------------- 1 | post = $post; 46 | $this->actor = $actor; 47 | $this->type = $type; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function getSubject() 54 | { 55 | return $this->post; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function getFromUser() 62 | { 63 | return $this->actor; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function getData() 70 | { 71 | return $this->type; 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public static function getType() 78 | { 79 | return 'vote'; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public static function getSubjectModel() 86 | { 87 | return Post::class; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Rank.php: -------------------------------------------------------------------------------- 1 | name = $name; 35 | $rank->color = $color; 36 | $rank->points = $points; 37 | 38 | return $rank; 39 | } 40 | 41 | /** 42 | * @param $name 43 | * 44 | * @return $this 45 | */ 46 | public function updateName($name) 47 | { 48 | $this->name = $name; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * @param $points 55 | * 56 | * @return $this 57 | */ 58 | public function updatePoints($points) 59 | { 60 | $this->points = $points; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * @param $color 67 | * 68 | * @return $this 69 | */ 70 | public function updateColor($color) 71 | { 72 | $this->color = $color; 73 | 74 | return $this; 75 | } 76 | 77 | public function users() 78 | { 79 | return $this->belongsToMany('Flarum\User\User', 'users_ranks'); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Validator/RankValidator.php: -------------------------------------------------------------------------------- 1 | ['required', 'string'], 21 | 'color' => ['required', 'string'], 22 | 'points' => ['required', 'integer'], 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /src/Vote.php: -------------------------------------------------------------------------------- 1 | post_id = $post->id; 34 | $vote->user_id = $user->id; 35 | 36 | return $vote; 37 | } 38 | } 39 | --------------------------------------------------------------------------------