├── LICENSE ├── README.md ├── composer.json ├── extend.php ├── js ├── admin.js ├── dist │ ├── admin.js │ ├── admin.js.LICENSE.txt │ ├── admin.js.map │ ├── forum.js │ └── forum.js.map ├── forum.js ├── package-lock.json ├── package.json ├── src │ ├── admin │ │ ├── components │ │ │ ├── BadgeActionDriverSettings.js │ │ │ ├── BadgeSelector.js │ │ │ ├── ConfirmModal.js │ │ │ ├── EditBadgeCategoryModal.js │ │ │ ├── EditBadgeModal.js │ │ │ ├── InstallAutoModerationMessage.js │ │ │ ├── SettingsPage.js │ │ │ └── SortableBadge.js │ │ ├── index.js │ │ └── utils │ │ │ └── loadBadges.js │ ├── common │ │ ├── components │ │ │ └── UserBadge.js │ │ ├── index.js │ │ └── models │ │ │ ├── Badge.js │ │ │ ├── BadgeCategory.js │ │ │ └── UserBadge.js │ └── forum │ │ ├── addBadgeListUserCard.js │ │ ├── addSidebarNav.js │ │ ├── components │ │ ├── BadgeCategoryList │ │ │ ├── BlockListView.js │ │ │ └── TableView.js │ │ ├── BadgeItemPage.js │ │ ├── BadgeModal.js │ │ ├── BadgeUserList.js │ │ ├── BadgesOverviewPage.js │ │ ├── BadgesProfilePage.js │ │ ├── GiveBadgeModal.js │ │ ├── SelectUserCardBadgesModal.js │ │ └── UserBadgeList.js │ │ ├── index.js │ │ ├── notification │ │ └── BadgeReceivedNotification.js │ │ ├── states │ │ └── UserBadgeListState.js │ │ └── utils │ │ └── categorizeUserBadges.js └── webpack.config.js ├── less ├── admin.less ├── admin │ ├── ConfirmModal.less │ ├── FlarumBadgeCategories.less │ └── SortableBadges.less ├── common │ └── UserBadge.less ├── forum.less └── forum │ ├── BadgeModal.less │ ├── BadgeOverviewPage.less │ ├── BadgePage.less │ ├── BadgeUserCard.less │ ├── BadgeUserList.less │ ├── Notification.less │ └── UserCardBadgesModal.less ├── locale └── en.yaml ├── migrations ├── 2021_02_23_16_07_add_badges_table.php ├── 2021_02_23_16_17_add_badges_user_table.php ├── 2021_02_23_16_19_add_badges_categories_table.php ├── 2021_02_23_16_23_add_badges_category_relationship.php ├── 2021_06_24_22_56_add_badges_category_is_table.php ├── 2021_06_24_23_00_add_badge_points.php ├── 2021_06_26_00_25_add_view_badge_users.php ├── 2021_11_25_21_01_add_badge_background_color.php ├── 2021_11_25_21_01_add_badge_icon_color.php ├── 2021_11_25_21_58_add_badge_label_color.php ├── 2022_07_01_16_22_add_badge_user_in_user_card.php ├── 2022_07_02_16_26_add_edit_own_user_card_badges.php └── 2022_07_02_16_27_add_edit_user_card_badges.php └── src ├── Access └── UserPolicy.php ├── Api ├── Controller │ ├── CreateBadgeCategoryController.php │ ├── CreateBadgeController.php │ ├── CreateUserBadgeController.php │ ├── DeleteBadgeCategoryController.php │ ├── DeleteBadgeController.php │ ├── DeleteUserBadgeController.php │ ├── ListBadgeCategoriesController.php │ ├── ListBadgesController.php │ ├── ListUserBadgesController.php │ ├── OrderBadgeCategoriesController.php │ ├── OrderBadgesController.php │ ├── ShowBadgeController.php │ ├── UpdateBadgeCategoryController.php │ ├── UpdateBadgeController.php │ └── UpdateUserBadgeController.php └── Serializer │ ├── BadgeCategorySerializer.php │ ├── BadgeSerializer.php │ └── UserBadgeSerializer.php ├── AutoModerator ├── Action │ ├── GiveBadge.php │ └── RemoveBadge.php ├── Metric │ └── BadgesReceived.php └── Requirement │ └── HasBadge.php ├── Badge ├── Badge.php ├── BadgeValidator.php └── Command │ ├── CreateBadge.php │ ├── CreateBadgeHandler.php │ ├── DeleteBadge.php │ ├── DeleteBadgeHandler.php │ ├── OrderBadges.php │ ├── OrderBadgesHandler.php │ ├── UpdateBadge.php │ └── UpdateBadgeHandler.php ├── BadgeCategory ├── BadgeCategory.php ├── BadgeCategoryValidator.php └── Command │ ├── CreateBadgeCategory.php │ ├── CreateBadgeCategoryHandler.php │ ├── DeleteBadgeCategory.php │ ├── DeleteBadgeCategoryHandler.php │ ├── OrderBadgeCategories.php │ ├── OrderBadgeCategoriesHandler.php │ ├── UpdateBadgeCategory.php │ └── UpdateBadgeCategoryHandler.php ├── Controllers └── BadgeOverviewController.php ├── Listeners └── SaveSelectedBadges.php ├── Notification └── BadgeReceivedBlueprint.php ├── Query └── FilterUserBadgesByBadge.php └── UserBadge ├── Command ├── CreateUserBadge.php ├── CreateUserBadgeHandler.php ├── DeleteUserBadge.php ├── DeleteUserBadgeHandler.php ├── UpdateUserBadge.php └── UpdateUserBadgeHandler.php ├── Filter └── UserBadgeFilterer.php ├── UserBadge.php ├── UserBadgeRepository.php └── UserBadgeValidator.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 V17 Development 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 | # 🔖 Flarum User Badges 2 | This Flarum extension will bring user badges to your forum. Reward your users with badges. 3 | 4 | If you have any feedback, let us know! Do you experience issues? You can report issues on the Flarum Forum or on [GitHub](https://github.com/v17development/flarum-user-badges). 5 | 6 | ## 📥 Installation 7 | If you like to install this extension, run the following command: 8 | ``` 9 | composer require v17development/flarum-user-badges 10 | ``` 11 | 12 | ## ♻ Updating 13 | Run the following command on your server to update the plugin 14 | ``` 15 | composer update v17development/flarum-user-badges 16 | ``` 17 | 18 | ## 🦸 Features 19 | - Create badge categories 20 | - Create badges 21 | - Add badges to users 22 | - Add badge earning reason 23 | - User badges page 24 | - Public badge list page 25 | - `Received new badge` notification 26 | - Fully integrated with Askvortsov's [Auto Moderator](https://discuss.flarum.org/d/27306) 27 | - Adds `Badges received` metric 28 | - Adds `Give badge` action 29 | - Adds `Remove badge` action 30 | - Adds `Has badge` requirement 31 | 32 | ## 📝 To-do: 33 | - User primary badge (next to name) 34 | - Image badges 35 | - Introduce 'relevant tag' badges (karma badges?): Automatic selecting 'primary' badges based on tags of a discussion 36 | 37 | ## 🙋 Questions, feedback? 38 | If you have any questions related to this extension, don't hesistate and send us an email, create an [issue on GitHub](https://github.com/v17development/flarum-user-badges) or ask your question on our [V17 Development Slack workspace](https://join.slack.com/t/v17dev/shared_invite/zt-g6ky1fd3-RreB9UB~636jL~QjDGfZHg). 39 | 40 | ## 🖼️ Screenshots 41 | 42 | ### User Badges list 43 | ![User Badges list](https://i.imgur.com/yi48Mbw.png) 44 | 45 | ## User badge notifications 46 | ![User badge notifications](https://i.imgur.com/0aVQ0LB.png) 47 | 48 | ## Public badges overview 49 | ![Public badges overview](https://i.imgur.com/r6lNDRf.png) 50 | 51 | ## Public badge detail page 52 | ![Public badge detail page](https://i.imgur.com/r1Vqej9.png) 53 | 54 | ### Admin badge management 55 | ![Admin badge management](https://i.imgur.com/bji13xf.png) 56 | 57 | ### Managing badges 58 | ![Admin badge management](https://i.imgur.com/jfZ6uSL.png) 59 | 60 | ### User badge info 61 | Admin rights & public info 62 | ![Admin rights & public info](https://i.imgur.com/XVKIZje.png) 63 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v17development/flarum-user-badges", 3 | "description": "Adds user badges to your Flarum community", 4 | "type": "flarum-extension", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "V17 Development", 9 | "email": "info@v17.dev" 10 | } 11 | ], 12 | "homepage": "https://v17.dev/projects/flarum-user-badges", 13 | "support": { 14 | "forum": "https://discuss.flarum.org/d/26449", 15 | "issues": "https://github.com/v17development/flarum-user-badges/issues", 16 | "source": "https://github.com/v17development/flarum-user-badges", 17 | "docs": "https://community.v17.dev/knowledgebase/category/flarum-user-badges" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "V17Development\\FlarumUserBadges\\": "src/" 22 | } 23 | }, 24 | "require": { 25 | "ext-json": "*", 26 | "flarum/core": "^1.3.1" 27 | }, 28 | "suggest": { 29 | "askvortsov/flarum-auto-moderator": "Allows you to automatically hand out badges to users" 30 | }, 31 | "extra": { 32 | "flarum-extension": { 33 | "title": "User badges", 34 | "category": "feature", 35 | "icon": { 36 | "name": "fas fa-user-tag", 37 | "backgroundColor": "#00ccb3", 38 | "color": "#225650" 39 | }, 40 | "optional-dependencies": [ 41 | "askvortsov/flarum-auto-moderator" 42 | ] 43 | }, 44 | "flarum-cli": { 45 | "modules": { 46 | "admin": true, 47 | "forum": true, 48 | "js": true, 49 | "jsCommon": true, 50 | "css": true, 51 | "gitConf": true, 52 | "githubActions": true, 53 | "prettier": true, 54 | "typescript": false, 55 | "bundlewatch": false, 56 | "backendTesting": false, 57 | "editorConfig": true, 58 | "styleci": true 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /extend.php: -------------------------------------------------------------------------------- 1 | js(__DIR__ . '/js/dist/forum.js') 17 | ->css(__DIR__ . '/less/forum.less') 18 | ->route('/badges', 'badges.overview', Controllers\BadgeOverviewController::class) 19 | ->route('/badges/{id}', 'badges.item', Controllers\BadgeOverviewController::class), 20 | (new Extend\Frontend('admin')) 21 | ->js(__DIR__ . '/js/dist/admin.js') 22 | ->css(__DIR__ . '/less/admin.less'), 23 | 24 | (new Extend\Routes('api')) 25 | // Badges 26 | ->get('/badges', 'badges.overview', Api\Controller\ListBadgesController::class) 27 | ->post('/badges', 'badges.create', Api\Controller\CreateBadgeController::class) 28 | ->post('/badges/order', 'badges.order', Api\Controller\OrderBadgesController::class) 29 | ->get('/badges/{id}', 'badges.show', Api\Controller\ShowBadgeController::class) 30 | ->patch('/badges/{id}', 'badges.update', Api\Controller\UpdateBadgeController::class) 31 | ->delete('/badges/{id}', 'badges.delete', Api\Controller\DeleteBadgeController::class) 32 | 33 | // Badge categories 34 | ->get('/badge_categories', 'badge.categories.overview', Api\Controller\ListBadgeCategoriesController::class) 35 | ->post('/badge_categories', 'badge.categories.create', Api\Controller\CreateBadgeCategoryController::class) 36 | ->post('/badge_categories/order', 'badge.categories.order', Api\Controller\OrderBadgeCategoriesController::class) 37 | ->patch('/badge_categories/{id}', 'badge.categories.update', Api\Controller\UpdateBadgeCategoryController::class) 38 | ->delete('/badge_categories/{id}', 'badge.categories.delete', Api\Controller\DeleteBadgeCategoryController::class) 39 | 40 | // User badges 41 | ->get('/user_badges', 'badge.users.overview', Api\Controller\ListUserBadgesController::class) 42 | ->post('/user_badges', 'badge.users.create', Api\Controller\CreateUserBadgeController::class) 43 | ->patch('/user_badges/{id}', 'badge.users.update', Api\Controller\UpdateUserBadgeController::class) 44 | ->delete('/user_badges/{id}', 'badge.users.delete', Api\Controller\DeleteUserBadgeController::class), 45 | 46 | // Extension permissions 47 | (new Extend\ApiSerializer(ForumSerializer::class)) 48 | ->attribute('canGiveBadge', function (ForumSerializer $serializer) { 49 | return $serializer->getActor()->hasPermission("badges.giveBadge"); 50 | }) 51 | ->attribute('canViewDetailedBadgeUsers', function (ForumSerializer $serializer) { 52 | return $serializer->getActor()->hasPermission("badges.canViewDetailedUsers"); 53 | }) 54 | ->attribute('editOwnUserCardBadges', function (ForumSerializer $serializer) { 55 | return $serializer->getActor()->hasPermission("badges.editOwnUserCardBadges"); 56 | }) 57 | ->attribute('editUserCardBadges', function (ForumSerializer $serializer) { 58 | return $serializer->getActor()->hasPermission("badges.editUserCardBadges"); 59 | }), 60 | 61 | // Badges relation with User 62 | (new Extend\Model(User::class)) 63 | ->relationship('userBadges', function ($user) { 64 | return $user->hasMany(UserBadge\UserBadge::class, 'user_id'); 65 | }) 66 | ->relationship('userPrimaryBadge', function ($user) { 67 | return $user->hasOne(UserBadge\UserBadge::class, 'user_id', null)->where('is_primary', true); 68 | }), 69 | 70 | (new Extend\ApiSerializer(UserSerializer::class)) 71 | ->hasMany('userBadges', Api\Serializer\UserBadgeSerializer::class) 72 | ->hasOne('userPrimaryBadge', Api\Serializer\UserBadgeSerializer::class), 73 | 74 | (new Extend\ApiSerializer(BasicUserSerializer::class)) 75 | ->hasMany('userBadges', Api\Serializer\UserBadgeSerializer::class), 76 | 77 | (new Extend\ApiController(FlarumController\ListUsersController::class)) 78 | ->addInclude(['userBadges', 'userBadges.badge']), 79 | (new Extend\ApiController(FlarumController\ShowUserController::class)) 80 | ->addInclude(['userBadges', 'userBadges.badge', 'userBadges.badge.category']), 81 | (new Extend\ApiController(FlarumController\UpdateUserController::class)) 82 | ->addInclude(['userBadges', 'userBadges.badge', 'userBadges.badge.category']), 83 | (new Extend\ApiController(FlarumController\CreateUserController::class)) 84 | ->addInclude(['userBadges', 'userBadges.badge', 'userBadges.badge.category']), 85 | (new Extend\ApiController(FlarumController\ShowDiscussionController::class)) 86 | ->addInclude(['posts.user.userBadges', 'posts.user.userBadges.badge']), 87 | (new Extend\ApiController(FlarumController\ListDiscussionsController::class)) 88 | ->addInclude(['user.userBadges', 'user.userBadges.badge']), 89 | 90 | (new Extend\Filter(UserBadge\Filter\UserBadgeFilterer::class)) 91 | ->addFilter(Query\FilterUserBadgesByBadge::class), 92 | 93 | (new Extend\Notification()) 94 | ->type(Notification\BadgeReceivedBlueprint::class, Api\Serializer\UserBadgeSerializer::class, ['alert']), 95 | 96 | new Extend\Locales(__DIR__ . '/locale'), 97 | 98 | (new Extend\Event()) 99 | ->listen(Saving::class, Listeners\SaveSelectedBadges::class), 100 | 101 | (new Extend\Policy()) 102 | ->modelPolicy(User::class, Access\UserPolicy::class), 103 | 104 | (new Extend\Settings) 105 | ->serializeToForum('showBadgesOnUserCard', 'v17development-user-badges.show_badges_on_user_card', 'boolval') 106 | ->serializeToForum('numberOfBadgesOnUserCard', 'v17development-user-badges.number_of_badges_on_user_card', 'intval') 107 | ->default('v17development-user-badges.number_of_badges_on_user_card', 5), 108 | ]; 109 | 110 | /** 111 | * Initialize Auto Moderator functionalities when installed 112 | */ 113 | if (class_exists("Askvortsov\AutoModerator\Extend\AutoModerator")) { 114 | $extend[] = 115 | (new AutoModeratorExtender()) 116 | ->actionDriver('give_badge', AutoModerator\Action\GiveBadge::class) 117 | ->actionDriver('remove_badge', AutoModerator\Action\RemoveBadge::class) 118 | ->requirementDriver('has_badge', AutoModerator\Requirement\HasBadge::class) 119 | ->metricDriver('badges_received', AutoModerator\Metric\BadgesReceived::class); 120 | } 121 | 122 | return $extend; 123 | -------------------------------------------------------------------------------- /js/admin.js: -------------------------------------------------------------------------------- 1 | export * from './src/common'; 2 | export * from './src/admin'; 3 | -------------------------------------------------------------------------------- /js/dist/admin.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /**! 2 | * Sortable 1.14.0 3 | * @author RubaXa 4 | * @author owenm 5 | * @license MIT 6 | */ 7 | -------------------------------------------------------------------------------- /js/dist/forum.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={n:t=>{var a=t&&t.__esModule?()=>t.default:()=>t;return e.d(a,{a}),a},d:(t,a)=>{for(var r in a)e.o(a,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:a[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};(()=>{"use strict";e.r(t);const a=flarum.core.compat["common/app"];e.n(a)().initializers.add("v17development/flarum-user-badges",(function(){}));const r=flarum.core.compat.extend,s=flarum.core.compat.Model;var n=e.n(s);const o=flarum.core.compat["models/User"];var i=e.n(o);const d=flarum.core.compat["components/UserPage"];var l=e.n(d);const u=flarum.core.compat["utils/UserControls"];var c=e.n(u);const p=flarum.core.compat["components/LinkButton"];var g=e.n(p);const f=flarum.core.compat["components/Button"];var b=e.n(f);function h(e,t){return h=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},h(e,t)}function v(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,h(e,t)}const y=flarum.core.compat["utils/mixin"];var B=e.n(y);const N=flarum.core.compat["components/Modal"];var _=e.n(N);const C=flarum.core.compat["helpers/fullTime"];var w=e.n(C);const U=flarum.core.compat["utils/ItemList"];var I=e.n(U);flarum.core.compat["components/Select"];const x=flarum.core.compat["utils/Stream"];var M=e.n(x),O=function(e){function t(){return e.apply(this,arguments)||this}v(t,e);var a=t.prototype;return a.oninit=function(t){var a=this;e.prototype.oninit.call(this,t),this.selectedBadge=this.attrs.badge?this.attrs.badge.badge():null,this.user=this.attrs.badge?this.attrs.badge.user():this.attrs.user,this.userHasBadge=!1,this.badge=this.attrs.badge?this.attrs.badge:app.store.createRecord("userBadges"),this.description=M()(this.badge.description()),this.categories={},this.uncategorizedBadges=[],this.loading=!1,this.attrs.badge||(this.loading=!0,app.store.find("badges",{include:"category"}).then((function(e){e.forEach((function(e){if(e.category()){var t=e.category();a.categories[t.id()]?a.categories[t.id()].badges.push(e):a.categories[t.id()]={category:t,badges:[e]}}else a.uncategorizedBadges.push(e)})),a.loading=!1,m.redraw()})))},a.className=function(){return"Modal--small BadgeModal"},a.title=function(){return app.translator.trans("v17development-flarum-badges.forum."+(this.badge.exists?"update":"give")+"_badge")},a.content=function(){return m("div",null,m("div",{className:"Modal-body"},m("div",{className:"Form"},this.fields().toArray())),m("div",{className:"Modal-footer"},m(b(),{className:"Button Button--primary",type:"submit",loading:this.loading,disabled:this.userHasBadge},app.translator.trans("core.forum.composer_edit.submit_button"))))},a.fields=function(){var e=this,t=new(I());return t.add("badge",m("div",{className:"BadgeModalListItem"},m("p",null,m("b",null,app.translator.trans("v17development-flarum-badges.forum.badge.badge"))),m("div",{className:"Select"},m("select",{value:this.selectedBadge?this.selectedBadge.id():"empty",disabled:!!this.attrs.badge,onchange:function(t){"empty"!==t.target.value&&(e.selectedBadge=app.store.getById("badges",t.target.value),e.checkUserHasBadge(e.selectedBadge))},className:"Select-input FormControl"},m("option",{value:"empty"},app.translator.trans("v17development-flarum-badges.forum.select_badge")),!this.attrs.badge&&Object.values(this.categories).map((function(e){var t=e.category,a=e.badges;return m("optgroup",{label:t.name()},a.map((function(e){return m("option",{value:e.id()},e.name())})))})),!this.attrs.badge&&this.uncategorizedBadges.length>=1&&m("optgroup",{label:app.translator.trans("v17development-flarum-badges.forum.uncategorized")},this.uncategorizedBadges.map((function(e){return m("option",{value:e.id()},e.name())}))),!!this.attrs.badge&&m("option",{value:this.selectedBadge.id()},this.selectedBadge.name())),m("i",{class:"icon fas fa-caret-down Select-caret"})),this.userHasBadge&&m("p",{className:"UserHasBadge"},app.translator.trans("v17development-flarum-badges.forum.user_has_badge"))),30),t.add("badge_description",m("div",{className:"BadgeModalListItem"},m("p",null,m("b",null,app.translator.trans("v17development-flarum-badges.forum.badge.description"),":")),m("p",null,this.selectedBadge?this.selectedBadge.description():app.translator.trans("v17development-flarum-badges.forum.select_badge"))),30),t.add("description",m("div",{className:"BadgeModalListItem"},m("p",null,m("b",null,app.translator.trans("v17development-flarum-badges.forum.badge.earning_reason"))),m("textarea",{className:"FormControl",placeholder:app.translator.trans("v17development-flarum-badges.forum.badge.earning_reason"),bidi:this.description})),30),t},a.checkUserHasBadge=function(e){var t=!1;this.user.userBadges().map((function(a){a.badge()==e&&(t=!0)})),this.userHasBadge=t,m.redraw()},a.onsubmit=function(e){var t=this;e.preventDefault(),this.loading=!0,this.badge.save({description:this.description(),relationships:this.attrs.badge?{}:{badge:this.selectedBadge,user:this.user}}).then((function(){t.attrs.badge?app.modal.show(k,{badge:t.attrs.badge.badge(),userBadgeData:t.attrs.badge}):t.hide(),m.redraw()}),(function(e){t.loading=!1,t.handleErrors(e)}))},t}(_()),k=function(e){function t(){return e.apply(this,arguments)||this}v(t,e);var a=t.prototype;return a.oninit=function(t){e.prototype.oninit.call(this,t),this.loading=!1},a.className=function(){return"Modal--small"},a.title=function(){return app.translator.trans("v17development-flarum-badges.forum.badge_information")},a.content=function(){var e=this;return m("div",null,m("div",{className:"Modal-body"},this.data().toArray()),m("div",{className:"Modal-footer"},m(g(),{href:app.route("badges.item",{id:this.attrs.badge.id()}),className:"Button",style:{margin:"0 10px"}},app.translator.trans("v17development-flarum-badges.forum.badge.badge_details")),this.attrs.userBadgeData&&app.forum.attribute("canGiveBadge")&&m(b(),{className:"Button Button--primary",onclick:function(){confirm(app.translator.trans("v17development-flarum-badges.forum.moderation.remove_badge_confirm"))&&(e.loading=!0,e.attrs.userBadgeData.delete().then((function(){return e.hide()})))},loading:this.loading},app.translator.trans("v17development-flarum-badges.forum.moderation.remove_badge"))))},a.data=function(){var e=this,t=new(I());return t.add("name",m("div",{className:"BadgeModalListItem"},m("p",null,m("b",null,app.translator.trans("v17development-flarum-badges.forum.badge.name"),":")),m("p",null,this.attrs.badge.name()))),t.add("description",m("div",{className:"BadgeModalListItem"},m("p",null,m("b",null,app.translator.trans("v17development-flarum-badges.forum.badge.description"),":")),m("p",null,this.attrs.badge.description()))),this.attrs.userBadgeData&&(this.attrs.userBadgeData.description()||app.forum.attribute("canGiveBadge"))&&t.add("earning_reason",m("div",{className:"BadgeModalListItem"},m("p",null,m("b",null,app.translator.trans("v17development-flarum-badges.forum.badge.earning_reason"),":")),m("p",null,this.attrs.userBadgeData.description()?this.attrs.userBadgeData.description():m("i",null,app.translator.trans("v17development-flarum-badges.forum.badge.no_earning_reason"))),m("p",null,app.forum.attribute("canGiveBadge")&&m("a",{href:"#",onclick:function(t){t.preventDefault(),app.modal.show(O,{badge:e.attrs.userBadgeData})}},app.translator.trans("v17development-flarum-badges.forum.badge.update_earning_reason"))))),this.attrs.userBadgeData&&t.add("earned_date",m("div",{className:"BadgeModalListItem"},m("p",null,m("b",null,app.translator.trans("v17development-flarum-badges.forum.badge.earned_on"),":")),m("p",null,w()(this.attrs.userBadgeData.assignedAt())))),this.attrs.userBadgeData&&t.add("category",m("div",{className:"BadgeModalListItem"},m("p",null,m("b",null,app.translator.trans("v17development-flarum-badges.forum.badge.category"),":")),m("p",null,this.attrs.badge.category()&&this.attrs.badge.category().name(),!this.attrs.badge.category()&&app.translator.trans("v17development-flarum-badges.forum.uncategorized")))),this.attrs.badge&&this.attrs.badge.earnedAmount()&&t.add("earned_amount",m("div",{className:"BadgeModalListItem"},m("p",null,app.translator.trans("v17development-flarum-badges.forum.badge.earned_count",{count:this.attrs.badge.earnedAmount()})))),t},t}(_()),L=function(e){function t(){return e.apply(this,arguments)||this}return v(t,e),t.prototype.apiEndpoint=function(){return"/badges"+(this.exists?"/"+this.data.id:"")},t}(B()(n(),{name:n().attribute("name"),icon:n().attribute("icon"),order:n().attribute("order"),image:n().attribute("image"),description:n().attribute("description"),isVisible:n().attribute("isVisible"),createdAt:n().attribute("createdAt"),earnedAmount:n().attribute("earnedAmount"),category:n().hasOne("category"),backgroundColor:n().attribute("backgroundColor"),iconColor:n().attribute("iconColor"),labelColor:n().attribute("labelColor")})),P=function(e){function t(){return e.apply(this,arguments)||this}return v(t,e),t.prototype.apiEndpoint=function(){return"/badge_categories"+(this.exists?"/"+this.data.id:"")},t}(B()(n(),{name:n().attribute("name"),order:n().attribute("order"),description:n().attribute("description"),isEnabled:n().attribute("isEnabled"),createdAt:n().attribute("createdAt"),isTable:n().attribute("isTable"),users:n().hasMany("users"),badges:n().hasMany("badges")})),A=function(e){function t(){return e.apply(this,arguments)||this}return v(t,e),t.prototype.apiEndpoint=function(){return"/user_badges"+(this.exists?"/"+this.data.id:"")},t}(B()(n(),{user:n().hasOne("user"),badge:n().hasOne("badge"),description:n().attribute("description"),isPrimary:n().attribute("isPrimary"),assignedAt:n().attribute("assignedAt"),inUserCard:n().attribute("inUserCard")}));const D=flarum.core.compat["components/LoadingIndicator"];var T=e.n(D);const S=flarum.core.compat["common/Component"];var z=e.n(S);const j=flarum.core.compat["common/components/Tooltip"];var E=e.n(j),V=function(e){function t(){return e.apply(this,arguments)||this}v(t,e);var a=t.prototype;return a.oninit=function(t){e.prototype.oninit.call(this,t),this.tooltip=!1!==this.attrs.tooltip,this.forceVisibility=!0===this.attrs.forceVisibility},a.view=function(){return this.attrs.badge.isVisible()||this.forceVisibility?!1===this.tooltip?this.badge():m(E(),{text:""+(this.attrs.badge.description()?this.attrs.badge.description():"")},this.badge()):null},a.badge=function(){var e=this,t=!this.attrs.badge.isVisible()&&this.forceVisibility;return this.attrs.badge.image()?m("img",{src:this.attrs.badge.image(),className:"UserBadgeImage",onclick:function(){e.attrs.onclick&&e.attrs.onclick()},style:{opacity:t?.5:void 0}}):m("span",{className:"UserBadge UserBadge-"+this.attrs.badge.id(),onclick:function(){e.attrs.onclick&&e.attrs.onclick()},style:{backgroundColor:this.attrs.badge.backgroundColor(),color:this.attrs.badge.labelColor(),borderColor:this.attrs.badge.backgroundColor(),opacity:t?.5:void 0}},m("i",{className:this.attrs.badge.icon(),style:{color:this.attrs.badge.iconColor()}})," ",this.attrs.badge.name())},t}(z());function H(e){var t={},a=[];e.userBadges().map((function(e){if(!e)return null;if(e.badge().category()){var r=e.badge().category();t[r.id()]?t[r.id()].badges.push(e):t[r.id()]={name:r.name(),category:r,badges:[e]}}else a.push(e)}));var r=Object.keys(t).sort((function(e,a){return t[e].category.order()-t[a].category.order()})).map((function(e){return t[e]}));return a.length>=1&&r.push({name:app.translator.trans("v17development-flarum-badges.forum.uncategorized"),category:null,badges:a}),r}var G=function(e){function t(){return e.apply(this,arguments)||this}return v(t,e),t.prototype.view=function(){var e=H(this.attrs.user);return m("div",{className:"UserBadges"},0===e.length&&m("div",{className:"Placeholder"},m("p",null,app.translator.trans("v17development-flarum-badges.forum.user_no_badges"))),e.length>=1&&e.map((function(e){var t=e.name,a=e.category,r=e.badges;return a&&!a.isEnabled()?null:m("div",{className:"UserBadgesCategory"},m("h3",null,t),a&&a.description()&&m("p",null,a.description()),r.sort((function(e,t){return e.badge().order()-t.badge().order()})).map((function(e){return m(V,{badge:e.badge(),onclick:function(){return app.modal.show(k,{badge:e.badge(),userBadgeData:e})}})})))})))},t}(z()),R=function(e){function t(){return e.apply(this,arguments)||this}v(t,e);var a=t.prototype;return a.oninit=function(t){e.prototype.oninit.call(this,t),this.user=null,this.loading=!0,this.loadUser(m.route.param("username"))},a.content=function(){return!this.user||this.loading?m(T(),{size:46}):G.component({user:this.user})},a.show=function(t){var a=this;e.prototype.show.call(this,t),this.user=t,app.store.find("users",t.id(),{include:"userBadges,userBadges.badge,userBadges.badge.category"}).then((function(){a.loading=!1,m.redraw()}))},t}(l());const F=flarum.core.compat["components/Page"];var q=e.n(F);const J=flarum.core.compat["components/IndexPage"];var K=e.n(J);const Q=flarum.core.compat["common/helpers/listItems"];var W=e.n(Q);const X=flarum.core.compat["components/Link"];var Y=e.n(X),Z=function(e){function t(){return e.apply(this,arguments)||this}return v(t,e),t.prototype.view=function(){if(!this.attrs.badges)return null;var e=this.attrs.badges;return m("table",{width:"100%",className:"BadgeTable"},m("thead",null,m("tr",null,m("th",{scope:"col"},app.translator.trans("v17development-flarum-badges.forum.badge.badges")),m("th",{scope:"col"},app.translator.trans("v17development-flarum-badges.forum.badge.description")))),m("tbody",null,e.map((function(e){return m("tr",null,m("td",null,m(Y(),{href:app.route("badges.item",{id:e.id()})},m(V,{badge:e,tooltip:!1}))),m("td",null,e.description(),m("div",{className:"BadgeTableRewarded"},app.translator.trans("v17development-flarum-badges.forum.badge.earned_count",{count:e.earnedAmount()}))))}))))},t}(z()),$=function(e){function t(){return e.apply(this,arguments)||this}return v(t,e),t.prototype.view=function(){if(!this.attrs.badges)return null;var e=this.attrs.badges;return m("ul",{className:"BadgeCategoryList"},e.map((function(e){return m("li",null,m(Y(),{href:app.route("badges.item",{id:e.id()}),className:"BadgeContainer"},m("div",{className:"BadgeContainerInfo"},m(V,{badge:e,tooltip:!1}),m("p",{className:"BadgeDescription"},e.description()),m("p",null,app.translator.trans("v17development-flarum-badges.forum.badge.earned_count",{count:e.earnedAmount()})))))})))},t}(z()),ee=function(e){function t(){return e.apply(this,arguments)||this}v(t,e);var a=t.prototype;return a.oninit=function(t){var a=this;e.prototype.oninit.call(this,t),this.bodyClass="App--index",this.loading=!0,app.history.push("badgeOverviewPage"),app.setTitle(app.translator.trans("v17development-flarum-badges.forum.badge.badges")),app.store.find("badge_categories").then((function(){a.loading=!1,m.redraw()}))},a.view=function(){var e=app.store.all("badgeCategories").sort((function(e,t){return e.order()-t.order()}));return m("div",{className:"IndexPage"},K().prototype.hero(),m("div",{className:"container"},m("div",{className:"sideNavContainer"},m("nav",{className:"IndexPage-nav sideNav"},m("ul",null,W()(K().prototype.sidebarItems().toArray()))),m("div",{className:"IndexPage-results sideNavOffset"},m("h2",{className:"BadgeOverviewTitle"},app.translator.trans("v17development-flarum-badges.forum.badge.badges")),this.loading&&m(T(),{size:"large"}),!this.loading&&e.map((function(e){var t=e.badges().sort((function(e,t){return e.order()-t.order()}));return m("div",{className:"BadgeCategory"},m("h3",null,e.name()),e.description()&&m("p",null,e.description()),e.isTable()&&m(Z,{badges:t}),!e.isTable()&&m($,{badges:t}))}))))))},t}(q());const te=flarum.core.compat["helpers/avatar"];var ae=e.n(te);const re=flarum.core.compat["helpers/humanTime"];var se=e.n(re),ne=function(e){function t(){return e.apply(this,arguments)||this}v(t,e);var a=t.prototype;return a.oninit=function(t){e.prototype.oninit.call(this,t),this.attrs.state.refreshParams({filter:{badge:this.attrs.badgeId},sort:"-assignedAt"})},a.view=function(){var e=this.attrs.state,t=null;return e.isInitialLoading()||e.isLoadingNext()?t=T().component({size:"large"}):e.hasNext()&&(t=b().component({className:"Button",icon:"fas fa-chevron-down",onclick:e.loadNext.bind(e)},app.translator.trans("core.forum.discussion_list.load_more_button"))),e.isInitialLoading()&&e.isEmpty()?m(T(),null):e.isEmpty()?m("div",{className:"BadgeUserList-empty"},app.translator.trans("v17development-flarum-badges.forum.no_received")):m("div",null,m("ul",{className:"BadgeUserList"},e.getPages().map((function(e){return e.items.map((function(e){return m("li",null,m(Y(),{href:app.route("user.badges",{username:e.user().username()}),className:"BadgeUserList-user"},ae()(e.user()),m("div",{className:"BadgeUserList-userinfo"},m("h4",null,e.user().displayName()),m("p",null,app.translator.trans("v17development-flarum-badges.forum.badge.received_on",{date:se()(e.assignedAt())})))))}))}))),t&&m("div",{className:"SupportSearchList-loadMore"},t))},t}(z()),oe=function(e){function t(){return e.apply(this,arguments)||this}v(t,e);var a=t.prototype;return a.oninit=function(t){var a=this;e.prototype.oninit.call(this,t),this.bodyClass="App--index";var r=app.store.getById("badges",m.route.param("id"));this.loading=!r,r?(app.history.push("badgeItemPage",r.name()),this.setTitle(r)):app.store.find("badges/"+m.route.param("id")).then((function(e){a.loading=!1,app.history.push("badgeItemPage",e.name()),a.setTitle(e),m.redraw()}))},a.setTitle=function(e){app.setTitle(e.name()+" - "+app.translator.trans("v17development-flarum-badges.forum.badge.badges"))},a.view=function(){var e=app.store.getById("badges",m.route.param("id"));return m("div",{className:"IndexPage"},K().prototype.hero(),m("div",{className:"container"},m("div",{className:"sideNavContainer"},m("nav",{className:"IndexPage-nav sideNav"},m("ul",null,W()(K().prototype.sidebarItems().toArray()))),m("div",{className:"IndexPage-results sideNavOffset"},m(g(),{href:app.route("badges"),icon:"fas fa-chevron-left",className:"Button BadgesOverviewButton"},app.translator.trans("v17development-flarum-badges.forum.badge.badges")),this.loading&&m(T(),{size:"large"}),!this.loading&&m("div",{className:"BadgeUserListInfo"},m("i",{className:e.icon()}),m("div",{className:"BadgeUserListInfo-meta"},m("h3",null,e.name()),m("p",null,e.description()))),e&&m("h3",null,app.translator.trans("v17development-flarum-badges.forum.badge.earned_by_count",{count:e.earnedAmount()})),!this.loading&&app.forum.attribute("canViewDetailedBadgeUsers")&&m(ne,{state:app.userBadgeListState,badgeId:e.id()})))))},t}(q());function ie(){(0,r.extend)(K().prototype,"navItems",(function(e){return e.add("badges",m(g(),{icon:"fas fa-id-badge",href:app.route("badges")},app.translator.trans("v17development-flarum-badges.forum.badge.badges")),15),e}))}function de(e,t){for(var a=0;a=e.limit&&-1===e.selectedBadges.indexOf(t.id());return m(E(),{text:a?app.translator.trans("v17development-flarum-badges.forum.badges_in_card.hit_limit",{limit:app.forum.attribute("numberOfBadgesOnUserCard")}):"",position:"bottom"},m("div",{className:"UserCardBadgesModalCategory-badge",onclick:function(){if(!a){var r=e.selectedBadges.indexOf(t.id());r>=0?e.selectedBadges.splice(r,1):e.selectedBadges.push(t.id())}}},m("div",{className:"UserCardBadgesModalCategory-badge-preview"},m(V,{badge:t.badge(),onclick:function(){}})),m("div",{className:"UserCardBadgesModalCategory-badge-switch"},Ce().component({state:e.selectedBadges.indexOf(t.id())>=0,disabled:a}))))}))))})),t},a.save=function(){var e=this;this.loading=!0,this.attrs.user.save({userCardBadges:this.selectedBadges}).then((function(){e.attrs.user.userBadges().map((function(t){t.pushAttributes({inUserCard:e.selectedBadges.indexOf(t.id())>=0})})),e.hide()})).catch((function(){})).then((function(){e.loading=!1,m.redraw()}))},t}(_());const Ue=flarum.core.compat["forum/states/DiscussionListState"];var Ie=e.n(Ue);app.initializers.add("v17development-flarum-badges",(function(e){e.store.models.badges=L,e.store.models.badgeCategories=P,e.store.models.userBadges=A,i().prototype.userBadges=n().hasMany("userBadges"),i().prototype.userPrimaryBadge=n().hasOne("userPrimaryBadge"),e.routes["user.badges"]={path:"/u/:username/badges",component:R},e.routes.badges={path:"/badges",component:ee},e.routes["badges.item"]={path:"/badges/:id",component:oe},ie(),e.userBadgeListState=new ue({}),e.notificationComponents.badgeReceived=ce,(0,r.extend)(ge().prototype,"notificationTypes",(function(t){t.add("badgeReceived",{name:"badgeReceived",icon:"fas fa-user-tag",label:e.translator.trans("v17development-flarum-badges.forum.notification.settings")})})),(0,r.extend)(l().prototype,"navItems",(function(t){t.add("badges",g().component({href:e.route("user.badges",{username:this.user.username()}),name:"badges",icon:"fas fa-user-tag"},[e.translator.trans("v17development-flarum-badges.forum.badge.badges"),m("span",{className:"Button-badge"},this.user.userBadges().length)]),90)})),(0,r.extend)(c(),"moderationControls",(function(t,a){e.forum.attribute("canGiveBadge")&&t.add("test",m(b(),{icon:"fas fa-user-tag",onclick:function(){return e.modal.show(O,{user:a})}},e.translator.trans("v17development-flarum-badges.forum.give_badge")))})),(0,he.extend)(ye().prototype,"infoItems",(function(e){var t,a=this.attrs.user;if(be().forum.attribute("showBadgesOnUserCard")&&a.userBadges){var r=null!=(t=a.userBadges())?t:[];if(!(r.length<1)&&r){var s=be().forum.attribute("numberOfBadgesOnUserCard"),n=r.filter((function(e){return e.inUserCard()}));0===n.length&&(n=r.slice(0,s));var o=n.map((function(e){return m(V,{badge:e.badge(),onclick:function(){return be().modal.show(k,{badge:e.badge(),userBadgeData:e})}})}));if(o.length settings({ badge_id: val })} />; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /js/src/admin/components/BadgeSelector.js: -------------------------------------------------------------------------------- 1 | import Component from 'flarum/Component'; 2 | import Button from 'flarum/components/Button'; 3 | import Dropdown from 'flarum/components/Dropdown'; 4 | import icon from 'flarum/helpers/icon'; 5 | import loadAllBadges from '../utils/loadBadges'; 6 | 7 | export default class BadgeSelector extends Component { 8 | oninit(vnode) { 9 | super.oninit(vnode); 10 | 11 | this.loaded = false; 12 | 13 | // Load badges 14 | loadAllBadges(() => (this.loaded = true)); 15 | } 16 | 17 | view() { 18 | if (!this.loaded) { 19 | return ( 20 |
21 | 22 | 23 | 26 |
27 | ); 28 | } 29 | 30 | const badge = app.store.getById('badges', this.attrs.value); 31 | const label = badge 32 | ? [icon(badge.icon()), '\t', badge.name()] 33 | : app.translator.trans('v17development-flarum-badges.admin.auto_moderator.badge_selector.placeholder'); 34 | return ( 35 |
36 | 37 | 38 | {this.attrs.disabled ? ( 39 |
{label}
40 | ) : ( 41 | 42 | {app.store.all('badges').map((g) => 43 | Button.component( 44 | { 45 | active: badge && badge.id() === g.id(), 46 | disabled: badge && badge.id() === g.id(), 47 | icon: g.icon(), 48 | onclick: () => { 49 | this.attrs.onchange(g.id()); 50 | }, 51 | }, 52 | g.name() 53 | ) 54 | )} 55 | 56 | )} 57 |
58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /js/src/admin/components/ConfirmModal.js: -------------------------------------------------------------------------------- 1 | import Modal from 'flarum/components/Modal'; 2 | import ItemList from 'flarum/utils/ItemList'; 3 | import Button from 'flarum/components/Button'; 4 | import Switch from 'flarum/components/Switch'; 5 | import Stream from 'flarum/utils/Stream'; 6 | 7 | export default class ConfirmModal extends Modal { 8 | oninit(vnode) { 9 | super.oninit(vnode); 10 | 11 | this.loading = false; 12 | } 13 | 14 | className() { 15 | return 'Modal--small FlarumBadgesConfirmModal'; 16 | } 17 | 18 | title() { 19 | return app.translator.trans(`v17development-flarum-badges.admin.confirm_dialog.title`); 20 | } 21 | 22 | content() { 23 | return [ 24 |
25 |

{this.attrs.text}

26 |
, 27 | 28 |
29 | {Button.component( 30 | { 31 | className: 'Button', 32 | disabled: this.loading, 33 | onclick: () => this.hide(), 34 | }, 35 | app.translator.trans('v17development-flarum-badges.admin.confirm_dialog.no') 36 | )} 37 | {Button.component( 38 | { 39 | className: 'Button Button--primary', 40 | loading: this.loading, 41 | onclick: () => this.confirm(), 42 | }, 43 | app.translator.trans('v17development-flarum-badges.admin.confirm_dialog.yes') 44 | )} 45 |
, 46 | ]; 47 | } 48 | 49 | /** 50 | * Confirm 51 | */ 52 | confirm() { 53 | // Act as promise 54 | if (this.attrs.promise) { 55 | this.loading = true; 56 | 57 | this.attrs.onconfirm( 58 | () => this.hide(), 59 | () => { 60 | this.loading = false; 61 | m.redraw(); 62 | } 63 | ); 64 | return; 65 | } 66 | 67 | this.attrs.onconfirm(); 68 | this.hide(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /js/src/admin/components/EditBadgeCategoryModal.js: -------------------------------------------------------------------------------- 1 | import Modal from 'flarum/components/Modal'; 2 | import ItemList from 'flarum/utils/ItemList'; 3 | import Button from 'flarum/components/Button'; 4 | import Switch from 'flarum/components/Switch'; 5 | import Stream from 'flarum/utils/Stream'; 6 | 7 | export default class EditBadgeCategoryModal extends Modal { 8 | oninit(vnode) { 9 | super.oninit(vnode); 10 | 11 | // Badge model 12 | this.badgeCategory = this.attrs.badgeCategory ? this.attrs.badgeCategory : app.store.createRecord('badgeCategories'); 13 | 14 | // Name 15 | this.name = Stream(this.badgeCategory.name()); 16 | 17 | // Description 18 | this.description = Stream(this.badgeCategory.description()); 19 | 20 | // Is enabled 21 | this.isTable = Stream(this.badgeCategory.exists ? this.badgeCategory.isTable() : true); 22 | 23 | // Is enabled 24 | this.isEnabled = Stream(this.badgeCategory.exists ? this.badgeCategory.isEnabled() : true); 25 | } 26 | 27 | className() { 28 | return 'Modal--small'; 29 | } 30 | 31 | title() { 32 | return app.translator.trans(`v17development-flarum-badges.admin.${this.badgeCategory.exists ? 'update_category' : 'create_category'}`); 33 | } 34 | 35 | content() { 36 | return [ 37 |
38 |
{this.data().toArray()}
39 |
, 40 |
41 | {Button.component( 42 | { 43 | type: 'submit', 44 | className: 'Button Button--primary', 45 | loading: this.loading, 46 | }, 47 | app.translator.trans('core.admin.settings.submit_button') 48 | )} 49 |
, 50 | ]; 51 | } 52 | 53 | data() { 54 | const items = new ItemList(); 55 | 56 | items.add( 57 | 'name', 58 |
59 | 60 | 65 |
, 66 | 50 67 | ); 68 | 69 | items.add( 70 | 'description', 71 |
72 | 73 |