├── LICENSE ├── README.md ├── composer.json ├── extend.php ├── js ├── admin.js ├── dist │ ├── admin.js │ ├── admin.js.map │ ├── forum.js │ └── forum.js.map ├── forum.js ├── package-lock.json ├── package.json ├── tsconfig.json └── webpack.config.js ├── migrations ├── 2020_07_15_000000_add_default_settings.php └── 2021_02_18_000000_add_default_permissions.php ├── phpstan.neon ├── resources ├── less │ └── forum.less └── locale │ └── en.yml └── src ├── AddForumStats.php ├── Listener └── FlushStats.php └── Repository └── StatsRepository.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 FriendsOfFlarum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Forum Statistics Widget by FriendsOfFlarum 2 | 3 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) [![Latest Stable Version](https://img.shields.io/packagist/v/fof/forum-statistics-widget.svg)](https://packagist.org/packages/fof/forum-statistics-widget) [![OpenCollective](https://img.shields.io/badge/opencollective-fof-blue.svg)](https://opencollective.com/fof/donate) 4 | 5 | A [Flarum](http://flarum.org) extension. Add a forum statistics widget to the sidenav section of your Flarum forum. 6 | 7 | ### Installation 8 | 9 | Using composer: 10 | 11 | ```sh 12 | composer require fof/forum-statistics-widget:"*" 13 | ``` 14 | 15 | ### Updating 16 | Using composer: 17 | 18 | ```sh 19 | composer update fof/forum-statistics-widget 20 | ``` 21 | 22 | ### Screenshots 23 | ![FoF_Forum_Statistics_Widget_Frontend_View](https://user-images.githubusercontent.com/2059356/111257989-7a938480-85f2-11eb-819d-30b4caa15df4.png) 24 | 25 | ![FoF_Forum_Statistics_Widget_Admin_Settings](https://user-images.githubusercontent.com/2059356/111917897-ad0d0980-8a58-11eb-82f5-8cb736af6d7c.png) 26 | 27 | ### Links 28 | 29 | - [Packagist](https://packagist.org/packages/fof/forum-statistics-widget) 30 | - [GitHub](https://github.com/FriendsOfFlarum/forum-statistics-widget) 31 | 32 | [![OpenCollective](https://img.shields.io/badge/donate-friendsofflarum-44AEE5?style=for-the-badge&logo=open-collective)](https://opencollective.com/fof/donate) 33 | 34 | An extension by [FriendsOfFlarum](https://github.com/FriendsOfFlarum). 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fof/forum-statistics-widget", 3 | "description": "Add a forum statistics widget to the sidenav section of your Flarum forum.", 4 | "keywords": [ 5 | "flarum", 6 | "forum", 7 | "statistics", 8 | "widget" 9 | ], 10 | "type": "flarum-extension", 11 | "license": "MIT", 12 | "support": { 13 | "issues": "https://github.com/FriendsOfFlarum/forum-statistics-widget/issues", 14 | "source": "https://github.com/FriendsOfFlarum/forum-statistics-widget", 15 | "forum": "https://discuss.flarum.org/d/22380" 16 | }, 17 | "homepage": "https://friendsofflarum.org", 18 | "funding": [ 19 | { 20 | "type": "website", 21 | "url": "https://opencollective.com/fof/donate" 22 | } 23 | ], 24 | "require": { 25 | "flarum/core": "^1.2.0" 26 | }, 27 | "authors": [ 28 | { 29 | "name": "Christian Lopez", 30 | "email": "me@ralkage.com", 31 | "role": "Developer" 32 | } 33 | ], 34 | "autoload": { 35 | "psr-4": { 36 | "FoF\\ForumStatisticsWidget\\": "src/" 37 | } 38 | }, 39 | "extra": { 40 | "flarum-extension": { 41 | "title": "FoF Forum Statistics Widget", 42 | "category": "feature", 43 | "icon": { 44 | "name": "fas fa-chart-bar", 45 | "backgroundColor": "#e74c3c", 46 | "color": "#fff" 47 | } 48 | }, 49 | "flagrow": { 50 | "discuss": "https://discuss.flarum.org/d/22380" 51 | }, 52 | "flarum-cli": { 53 | "modules": { 54 | "githubActions": true 55 | } 56 | } 57 | }, 58 | "require-dev": { 59 | "flarum/phpstan": "*" 60 | }, 61 | "scripts": { 62 | "analyse:phpstan": "phpstan analyse", 63 | "clear-cache:phpstan": "phpstan clear-result-cache" 64 | }, 65 | "scripts-descriptions": { 66 | "analyse:phpstan": "Run static analysis" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /extend.php: -------------------------------------------------------------------------------- 1 | js(__DIR__.'/js/dist/forum.js') 20 | ->css(__DIR__.'/resources/less/forum.less'), 21 | 22 | (new Extend\Frontend('admin')) 23 | ->js(__DIR__.'/js/dist/admin.js'), 24 | 25 | new Extend\Locales(__DIR__.'/resources/locale'), 26 | 27 | (new Extend\Settings()) 28 | ->default('fof-forum-statistics-widget.ignore_private_discussions', false) 29 | ->default('fof-forum-statistics-widget.cache_duration', 600) 30 | ->default('fof-forum-statistics-widget.flush_cache_on_new_registration', false) 31 | ->serializeToForum( 32 | 'fof-forum-statistics-widget.widget_order', 33 | 'fof-forum-statistics-widget.widget_order', 34 | 'intval', 35 | 0 36 | ), 37 | 38 | (new Extend\ApiSerializer(ForumSerializer::class)) 39 | ->attributes(AddForumStats::class), 40 | 41 | (new Extend\Event()) 42 | ->subscribe(Listener\FlushStats::class), 43 | ]; 44 | -------------------------------------------------------------------------------- /js/admin.js: -------------------------------------------------------------------------------- 1 | export * from './src/admin'; 2 | -------------------------------------------------------------------------------- /js/dist/admin.js: -------------------------------------------------------------------------------- 1 | (()=>{var t={n:e=>{var s=e&&e.__esModule?()=>e.default:()=>e;return t.d(s,{a:s}),s},d:(e,s)=>{for(var i in s)t.o(s,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:s[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e);const s=flarum.core.compat["admin/app"];var i=t.n(s),r="fof-forum-statistics-widget.admin.";i().initializers.add("fof-forum-statistics-widget",(function(){i().extensionData.for("fof-forum-statistics-widget").registerSetting({label:i().translator.trans(r+"settings.widget_ignore_private_discussions_label"),setting:"fof-forum-statistics-widget.ignore_private_discussions",type:"switch"}).registerSetting({label:i().translator.trans(r+"settings.widget_order_label"),setting:"fof-forum-statistics-widget.widget_order",type:"number",help:i().translator.trans(r+"settings.widget_helper_text")}).registerSetting({setting:"fof-forum-statistics-widget.cache_duration",type:"number",min:0,label:i().translator.trans("fof-forum-statistics-widget.admin.settings.cache_duration_label"),help:i().translator.trans("fof-forum-statistics-widget.admin.settings.cache_duration_help")}).registerSetting({setting:"fof-forum-statistics-widget.flush_cache_on_new_registration",type:"boolean",label:i().translator.trans("fof-forum-statistics-widget.admin.settings.flush_cache_on_new_registration_label"),help:i().translator.trans("fof-forum-statistics-widget.admin.settings.flush_cache_on_new_registration_help")}).registerPermission({icon:"fas fa-chart-bar",label:i().translator.trans(r+"permissions.view_discussions_count_label"),permission:"fof-forum-statistics-widget.viewWidget.discussionsCount",allowGuest:!0},"view").registerPermission({icon:"fas fa-chart-bar",label:i().translator.trans(r+"permissions.view_posts_count_label"),permission:"fof-forum-statistics-widget.viewWidget.postsCount",allowGuest:!0},"view").registerPermission({icon:"fas fa-chart-bar",label:i().translator.trans(r+"permissions.view_users_count_label"),permission:"fof-forum-statistics-widget.viewWidget.usersCount",allowGuest:!0},"view").registerPermission({icon:"fas fa-chart-bar",label:i().translator.trans(r+"permissions.view_lastest_member_label"),permission:"fof-forum-statistics-widget.viewWidget.latestMember",allowGuest:!0},"view")}))})(),module.exports=e})(); 2 | //# sourceMappingURL=admin.js.map -------------------------------------------------------------------------------- /js/dist/admin.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,+BCL9D,MAAM,EAA+BC,OAAOC,KAAKC,OAAO,a,aCElDC,EAAoB,qCAE1BC,IAAAA,aAAiBC,IAAI,+BAA+B,WAClDD,IAAAA,cAAiB,IACV,+BACJE,gBAAgB,CACfC,MAAOH,IAAAA,WAAeI,MAAML,EAAoB,oDAChDM,QAAS,yDACTC,KAAM,WAEPJ,gBAAgB,CACfC,MAAOH,IAAAA,WAAeI,MAAML,EAAoB,+BAChDM,QAAS,2CACTC,KAAM,SACNC,KAAMP,IAAAA,WAAeI,MAAML,EAAoB,iCAEhDG,gBAAgB,CACfG,QAAS,6CACTC,KAAM,SACNE,IAAK,EACLL,MAAOH,IAAAA,WAAeI,MAAM,mEAC5BG,KAAMP,IAAAA,WAAeI,MAAM,oEAE5BF,gBAAgB,CACfG,QAAS,8DACTC,KAAM,UACNH,MAAOH,IAAAA,WAAeI,MAAM,oFAC5BG,KAAMP,IAAAA,WAAeI,MAAM,qFAE5BK,mBACC,CACEC,KAAM,mBACNP,MAAOH,IAAAA,WAAeI,MAAML,EAAoB,4CAChDY,WAAY,0DACZC,YAAY,GAEd,QAEDH,mBACC,CACEC,KAAM,mBACNP,MAAOH,IAAAA,WAAeI,MAAML,EAAoB,sCAChDY,WAAY,oDACZC,YAAY,GAEd,QAEDH,mBACC,CACEC,KAAM,mBACNP,MAAOH,IAAAA,WAAeI,MAAML,EAAoB,sCAChDY,WAAY,oDACZC,YAAY,GAEd,QAEDH,mBACC,CACEC,KAAM,mBACNP,MAAOH,IAAAA,WAAeI,MAAML,EAAoB,yCAChDY,WAAY,sDACZC,YAAY,GAEd,OAEN,G","sources":["webpack://@fof/forum-statistics-widget/webpack/bootstrap","webpack://@fof/forum-statistics-widget/webpack/runtime/compat get default export","webpack://@fof/forum-statistics-widget/webpack/runtime/define property getters","webpack://@fof/forum-statistics-widget/webpack/runtime/hasOwnProperty shorthand","webpack://@fof/forum-statistics-widget/webpack/runtime/make namespace object","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['admin/app']\"","webpack://@fof/forum-statistics-widget/./src/admin/index.ts"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/app'];","import app from 'flarum/admin/app';\n\nconst translationPrefix = 'fof-forum-statistics-widget.admin.';\n\napp.initializers.add('fof-forum-statistics-widget', () => {\n app.extensionData\n .for('fof-forum-statistics-widget')\n .registerSetting({\n label: app.translator.trans(translationPrefix + 'settings.widget_ignore_private_discussions_label'),\n setting: 'fof-forum-statistics-widget.ignore_private_discussions',\n type: 'switch',\n })\n .registerSetting({\n label: app.translator.trans(translationPrefix + 'settings.widget_order_label'),\n setting: 'fof-forum-statistics-widget.widget_order',\n type: 'number',\n help: app.translator.trans(translationPrefix + 'settings.widget_helper_text'),\n })\n .registerSetting({\n setting: 'fof-forum-statistics-widget.cache_duration',\n type: 'number',\n min: 0,\n label: app.translator.trans('fof-forum-statistics-widget.admin.settings.cache_duration_label'),\n help: app.translator.trans('fof-forum-statistics-widget.admin.settings.cache_duration_help'),\n })\n .registerSetting({\n setting: 'fof-forum-statistics-widget.flush_cache_on_new_registration',\n type: 'boolean',\n label: app.translator.trans('fof-forum-statistics-widget.admin.settings.flush_cache_on_new_registration_label'),\n help: app.translator.trans('fof-forum-statistics-widget.admin.settings.flush_cache_on_new_registration_help'),\n })\n .registerPermission(\n {\n icon: 'fas fa-chart-bar',\n label: app.translator.trans(translationPrefix + 'permissions.view_discussions_count_label'),\n permission: 'fof-forum-statistics-widget.viewWidget.discussionsCount',\n allowGuest: true,\n },\n 'view'\n )\n .registerPermission(\n {\n icon: 'fas fa-chart-bar',\n label: app.translator.trans(translationPrefix + 'permissions.view_posts_count_label'),\n permission: 'fof-forum-statistics-widget.viewWidget.postsCount',\n allowGuest: true,\n },\n 'view'\n )\n .registerPermission(\n {\n icon: 'fas fa-chart-bar',\n label: app.translator.trans(translationPrefix + 'permissions.view_users_count_label'),\n permission: 'fof-forum-statistics-widget.viewWidget.usersCount',\n allowGuest: true,\n },\n 'view'\n )\n .registerPermission(\n {\n icon: 'fas fa-chart-bar',\n label: app.translator.trans(translationPrefix + 'permissions.view_lastest_member_label'),\n permission: 'fof-forum-statistics-widget.viewWidget.latestMember',\n allowGuest: true,\n },\n 'view'\n );\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","core","compat","translationPrefix","app","add","registerSetting","label","trans","setting","type","help","min","registerPermission","icon","permission","allowGuest"],"sourceRoot":""} -------------------------------------------------------------------------------- /js/dist/forum.js: -------------------------------------------------------------------------------- 1 | (()=>{var t={n:o=>{var r=o&&o.__esModule?()=>o.default:()=>o;return t.d(r,{a:r}),r},d:(o,r)=>{for(var n in r)t.o(r,n)&&!t.o(o,n)&&Object.defineProperty(o,n,{enumerable:!0,get:r[n]})},o:(t,o)=>Object.prototype.hasOwnProperty.call(t,o),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},o={};(()=>{"use strict";t.r(o);const r=flarum.core.compat["forum/app"];var n=t.n(r);const e=flarum.core.compat["common/extend"],s=flarum.core.compat["forum/components/IndexPage"];var a=t.n(s);function u(t,o){return u=Object.setPrototypeOf||function(t,o){return t.__proto__=o,t},u(t,o)}const i=flarum.core.compat["common/Component"];var c=t.n(i);const l=flarum.core.compat["common/helpers/username"];var f=t.n(l);const d=flarum.core.compat["common/utils/formatNumber"];var p=t.n(d);const v=flarum.core.compat["common/components/Link"];var b=t.n(v);const g=flarum.core.compat["common/components/LoadingIndicator"];var y=t.n(g);const _=flarum.core.compat["common/utils/ItemList"];var h=t.n(_),w="fof-forum-statistics-widget.forum.navbar.",O="fof-forum-statistics-widget.",S=function(t){var o,r;function e(){return t.apply(this,arguments)||this}r=t,(o=e).prototype=Object.create(r.prototype),o.prototype.constructor=o,u(o,r);var s=e.prototype;return s.oninit=function(o){var r=this;t.prototype.oninit.call(this,o);var e=n().forum.attribute(O+"lastUserId");e&&(this.loading=!0,n().store.find("users",e).then((function(t){r.user=t,r.loading=!1,m.redraw()})))},s.view=function(){if(this.loading)return m(y(),null);var t=this.items().toArray();return 0!==t.length?m("div",{class:"ForumStatistics containerNarrow"},m("div",{class:"row"},m("h2",null,m("i",{class:"fas fa-chart-bar"})," ",n().translator.trans(w+"widget_title")),m("div",null,m("ul",{id:"ForumStatisticsList"},t)))):void 0},s.items=function(){var t=new(h()),o=n().forum.attribute(O+"discussionsCount"),r=n().forum.attribute(O+"postsCount"),e=n().forum.attribute(O+"usersCount"),s=n().forum.attribute(O+"lastUserId");return o&&t.add("discussions_count",m("li",null,n().translator.trans(w+"discussions_count",{discussionsCount:p()(o)}))),r&&t.add("posts_count",m("li",null,n().translator.trans(w+"posts_count",{postsCount:p()(r)}))),e&&t.add("users_count",m("li",null,n().translator.trans(w+"users_count",{usersCount:p()(e)}))),s&&t.add("latest_member",m("li",null,n().translator.trans(w+"latest_member")," ",m(b(),{href:n().route.user(this.user)},m("strong",null,f()(this.user))))),t},e}(c());n().initializers.add("fof/forum-statistics-widget",(function(){(0,e.extend)(a().prototype,"sidebarItems",(function(t){t.add("forumStatisticsWidget",S.component(),n().forum.attribute("fof-forum-statistics-widget.widget_order"))}))}))})(),module.exports=o})(); 2 | //# sourceMappingURL=forum.js.map -------------------------------------------------------------------------------- /js/dist/forum.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"forum.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,+BCL9D,MAAM,EAA+BC,OAAOC,KAAKC,OAAO,a,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,iBCAlD,EAA+BF,OAAOC,KAAKC,OAAO,8B,aCAzC,SAASC,EAAgBhB,EAAGiB,GAMzC,OALAD,EAAkBf,OAAOiB,gBAAkB,SAAyBlB,EAAGiB,GAErE,OADAjB,EAAEmB,UAAYF,EACPjB,CACT,EAEOgB,EAAgBhB,EAAGiB,EAC5B,CCPA,MAAM,EAA+BJ,OAAOC,KAAKC,OAAO,oB,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,2B,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,6B,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,0B,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,sC,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,yB,aCQlDK,EAAoB,4CACpBC,EAAkB,+BAEHC,EAAqB,SAAAC,GCV3B,IAAwBC,EAAUC,EDUP,SAAAH,IAAA,OAAAC,EAAAG,MAAA,KAAAC,YAAA,KCVOF,EDUPF,GCVHC,EDUGF,GCT/Bf,UAAYN,OAAO2B,OAAOH,EAAWlB,WAC9CiB,EAASjB,UAAUsB,YAAcL,EACjCN,EAAeM,EAAUC,GDOe,IAAAK,EAAAR,EAAAf,UA6FvC,OA7FuCuB,EACxCC,OAAA,SAAOC,GAAO,IAAAC,EAAA,KACZV,EAAAhB,UAAMwB,OAAMtB,KAAC,KAAAuB,GACb,IAAME,EAAaC,IAAAA,MAAUC,UAAUf,EAAkB,cAErDa,IACFG,KAAKC,SAAU,EAEfH,IAAAA,MAAUI,KAAK,QAASL,GAAYM,MAAK,SAACC,GACxCR,EAAKQ,KAAOA,EACZR,EAAKK,SAAU,EACfI,EAAEC,QACJ,IAEJ,EAACb,EAEDc,KAAA,WACE,GAAIP,KAAKC,QACP,OAAOI,EAACG,IAAgB,MAG1B,IAAMC,EAAQT,KAAKS,QAAQC,UAE3B,OAAqB,IAAjBD,EAAME,OAGRN,EAAA,OAAKO,MAAM,mCACTP,EAAA,OAAKO,MAAM,OACTP,EAAA,UACEA,EAAA,KAAGO,MAAM,qBAAuB,IAAEd,IAAAA,WAAee,MAAM9B,EAAoB,iBAE7EsB,EAAA,WACEA,EAAA,MAAIS,GAAG,uBAAuBL,WATtC,CAcF,EAAChB,EAEDgB,MAAA,WACE,IAAMA,EAAQ,IAAIM,KAEdC,EAAmBlB,IAAAA,MAAUC,UAAUf,EAAkB,oBACzDiC,EAAanB,IAAAA,MAAUC,UAAUf,EAAkB,cACnDkC,EAAapB,IAAAA,MAAUC,UAAUf,EAAkB,cACjDa,EAAaC,IAAAA,MAAUC,UAAUf,EAAkB,cA+CzD,OA7CIgC,GACFP,EAAMU,IACJ,oBACAd,EAAA,UACGP,IAAAA,WAAee,MAAM9B,EAAoB,oBAAqB,CAC7DiC,iBAAkBI,IAAaJ,OAMnCC,GACFR,EAAMU,IACJ,cACAd,EAAA,UACGP,IAAAA,WAAee,MAAM9B,EAAoB,cAAe,CACvDkC,WAAYG,IAAaH,OAM7BC,GACFT,EAAMU,IACJ,cACAd,EAAA,UACGP,IAAAA,WAAee,MAAM9B,EAAoB,cAAe,CACvDmC,WAAYE,IAAaF,OAM7BrB,GACFY,EAAMU,IACJ,gBACAd,EAAA,UACGP,IAAAA,WAAee,MAAM9B,EAAoB,iBAAkB,IAC5DsB,EAACgB,IAAI,CAACC,KAAMxB,IAAAA,MAAUM,KAAKJ,KAAKI,OAC9BC,EAAA,cAASkB,IAASvB,KAAKI,UAMxBK,CACT,EAACxB,CAAA,CA7FuC,CAASuC,KEJnD1B,IAAAA,aAAiBqB,IAAI,+BAA+B,YAClDM,EAAAA,EAAAA,QAAOC,IAAAA,UAAqB,gBAAgB,SAAUjB,GACpDA,EAAMU,IAAI,wBAAyBlC,EAAsB0C,YAAa7B,IAAAA,MAAUC,UAAU,4CAC5F,GACF,G","sources":["webpack://@fof/forum-statistics-widget/webpack/bootstrap","webpack://@fof/forum-statistics-widget/webpack/runtime/compat get default export","webpack://@fof/forum-statistics-widget/webpack/runtime/define property getters","webpack://@fof/forum-statistics-widget/webpack/runtime/hasOwnProperty shorthand","webpack://@fof/forum-statistics-widget/webpack/runtime/make namespace object","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['forum/app']\"","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['common/extend']\"","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['forum/components/IndexPage']\"","webpack://@fof/forum-statistics-widget/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['common/Component']\"","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['common/helpers/username']\"","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['common/utils/formatNumber']\"","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['common/components/Link']\"","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['common/components/LoadingIndicator']\"","webpack://@fof/forum-statistics-widget/external root \"flarum.core.compat['common/utils/ItemList']\"","webpack://@fof/forum-statistics-widget/./src/forum/components/ForumStatisticsWidget.js","webpack://@fof/forum-statistics-widget/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js","webpack://@fof/forum-statistics-widget/./src/forum/index.ts"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/IndexPage'];","export default function _setPrototypeOf(o, p) {\n _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {\n o.__proto__ = p;\n return o;\n };\n\n return _setPrototypeOf(o, p);\n}","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Component'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/username'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/formatNumber'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Link'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LoadingIndicator'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/ItemList'];","import app from 'flarum/forum/app';\nimport Component from 'flarum/common/Component';\nimport username from 'flarum/common/helpers/username';\nimport formatNumber from 'flarum/common/utils/formatNumber';\nimport Link from 'flarum/common/components/Link';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\nimport ItemList from 'flarum/common/utils/ItemList';\n\nconst translationPrefix = 'fof-forum-statistics-widget.forum.navbar.';\nconst attributePrefix = 'fof-forum-statistics-widget.';\n\nexport default class ForumStatisticsWidget extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n const lastUserId = app.forum.attribute(attributePrefix + 'lastUserId');\n\n if (lastUserId) {\n this.loading = true;\n\n app.store.find('users', lastUserId).then((user) => {\n this.user = user;\n this.loading = false;\n m.redraw();\n });\n }\n }\n\n view() {\n if (this.loading) {\n return ;\n }\n\n const items = this.items().toArray();\n\n if (items.length === 0) return;\n\n return (\n
\n
\n

\n {app.translator.trans(translationPrefix + 'widget_title')}\n

\n
\n
    {items}
\n
\n
\n
\n );\n }\n\n items() {\n const items = new ItemList();\n\n let discussionsCount = app.forum.attribute(attributePrefix + 'discussionsCount');\n let postsCount = app.forum.attribute(attributePrefix + 'postsCount');\n let usersCount = app.forum.attribute(attributePrefix + 'usersCount');\n const lastUserId = app.forum.attribute(attributePrefix + 'lastUserId');\n\n if (discussionsCount) {\n items.add(\n 'discussions_count',\n
  • \n {app.translator.trans(translationPrefix + 'discussions_count', {\n discussionsCount: formatNumber(discussionsCount),\n })}\n
  • \n );\n }\n\n if (postsCount) {\n items.add(\n 'posts_count',\n
  • \n {app.translator.trans(translationPrefix + 'posts_count', {\n postsCount: formatNumber(postsCount),\n })}\n
  • \n );\n }\n\n if (usersCount) {\n items.add(\n 'users_count',\n
  • \n {app.translator.trans(translationPrefix + 'users_count', {\n usersCount: formatNumber(usersCount),\n })}\n
  • \n );\n }\n\n if (lastUserId) {\n items.add(\n 'latest_member',\n
  • \n {app.translator.trans(translationPrefix + 'latest_member')}{' '}\n \n {username(this.user)}\n \n
  • \n );\n }\n\n return items;\n }\n}\n","import setPrototypeOf from \"./setPrototypeOf.js\";\nexport default function _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n setPrototypeOf(subClass, superClass);\n}","import app from 'flarum/forum/app';\nimport { extend } from 'flarum/common/extend';\nimport IndexPage from 'flarum/forum/components/IndexPage';\nimport ForumStatisticsWidget from './components/ForumStatisticsWidget';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport type Mithril from 'mithril';\n\napp.initializers.add('fof/forum-statistics-widget', () => {\n extend(IndexPage.prototype, 'sidebarItems', function (items: ItemList) {\n items.add('forumStatisticsWidget', ForumStatisticsWidget.component(), app.forum.attribute('fof-forum-statistics-widget.widget_order'));\n });\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","core","compat","_setPrototypeOf","p","setPrototypeOf","__proto__","translationPrefix","attributePrefix","ForumStatisticsWidget","_Component","subClass","superClass","apply","arguments","create","constructor","_proto","oninit","vnode","_this","lastUserId","app","attribute","this","loading","find","then","user","m","redraw","view","LoadingIndicator","items","toArray","length","class","trans","id","ItemList","discussionsCount","postsCount","usersCount","add","formatNumber","Link","href","username","Component","extend","IndexPage","component"],"sourceRoot":""} -------------------------------------------------------------------------------- /js/forum.js: -------------------------------------------------------------------------------- 1 | export * from './src/forum'; 2 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fof/forum-statistics-widget", 3 | "version": "0.0.0", 4 | "private": true, 5 | "prettier": "@flarum/prettier-config", 6 | "dependencies": { 7 | "@flarum/prettier-config": "^1.0.0", 8 | "flarum-tsconfig": "^1.0.2", 9 | "flarum-webpack-config": "^2.0.0", 10 | "webpack": "^5.89.0", 11 | "webpack-cli": "^5.1.4" 12 | }, 13 | "devDependencies": { 14 | "prettier": "^3.0.3" 15 | }, 16 | "scripts": { 17 | "dev": "webpack --mode development --watch", 18 | "build": "webpack --mode production", 19 | "format": "prettier --write src", 20 | "format-check": "prettier --check src" 21 | } 22 | } -------------------------------------------------------------------------------- /js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use Flarum's tsconfig as a starting point 3 | "extends": "flarum-tsconfig", 4 | // This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder 5 | // and also tells your Typescript server to read core's global typings for 6 | // access to `dayjs` and `$` in the global namespace. 7 | "include": ["src/**/*", "../vendor/flarum/core/js/dist-typings/@types/**/*"], 8 | "compilerOptions": { 9 | // This will output typings to `dist-typings` 10 | "declarationDir": "./dist-typings", 11 | "baseUrl": ".", 12 | "paths": { 13 | "flarum/*": ["../vendor/flarum/core/js/dist-typings/*"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('flarum-webpack-config')(); 2 | -------------------------------------------------------------------------------- /migrations/2020_07_15_000000_add_default_settings.php: -------------------------------------------------------------------------------- 1 | true, 16 | 'fof-forum-statistics-widget.widget_order' => 0, 17 | ]); 18 | -------------------------------------------------------------------------------- /migrations/2021_02_18_000000_add_default_permissions.php: -------------------------------------------------------------------------------- 1 | Group::GUEST_ID, 18 | 'fof-forum-statistics-widget.viewWidget.postsCount' => Group::GUEST_ID, 19 | 'fof-forum-statistics-widget.viewWidget.usersCount' => Group::GUEST_ID, 20 | 'fof-forum-statistics-widget.viewWidget.latestMember' => Group::GUEST_ID, 21 | ]); 22 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/flarum/phpstan/extension.neon 3 | 4 | parameters: 5 | # The level will be increased in Flarum 2.0 6 | level: 5 7 | paths: 8 | - extend.php 9 | - src 10 | excludePaths: 11 | - *.blade.php 12 | checkMissingIterableValueType: false 13 | databaseMigrationsPath: ['migrations'] 14 | -------------------------------------------------------------------------------- /resources/less/forum.less: -------------------------------------------------------------------------------- 1 | .ForumStatistics { 2 | color: @control-color; 3 | width: 100%; 4 | margin-top: 20px; 5 | } 6 | 7 | #ForumStatisticsList { 8 | margin: 0; 9 | padding: 0; 10 | list-style-type: none; 11 | position: relative; 12 | } 13 | -------------------------------------------------------------------------------- /resources/locale/en.yml: -------------------------------------------------------------------------------- 1 | fof-forum-statistics-widget: 2 | forum: 3 | navbar: 4 | widget_title: Forum Statistics 5 | discussions_count: "Discussions: {discussionsCount}" 6 | posts_count: "Posts: {postsCount}" 7 | users_count: "Members: {usersCount}" 8 | latest_member: "Latest Member: " 9 | admin: 10 | settings: 11 | cache_duration_label: Cache Duration 12 | cache_duration_help: "Specify how long the stats for the widget should be cached for (in seconds). 0 to disable caching (not recommended)." 13 | flush_cache_on_new_registration_label: Flush Stats Cache on New Registration 14 | flush_cache_on_new_registration_help: "If enabled, the stats cache will be flushed when a new user registers on the forum." 15 | widget_ignore_private_discussions_label: Ignore Private Discussions 16 | widget_order_label: Widget Order 17 | widget_helper_text: "Edit this setting to adjust this widget's position on the sidenav section of your Flarum forum. 18 | The higher the number, the higher this widget will be on the sidenav items list (this value can be either positive or negative)." 19 | permissions: 20 | view_discussions_count_label: View forum statistics widget discussions count 21 | view_posts_count_label: View forum statistics widget posts count 22 | view_users_count_label: View forum statistics widget users count 23 | view_lastest_member_label: View forum statistics widget latest member 24 | -------------------------------------------------------------------------------- /src/AddForumStats.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 42 | $this->settings = $settings; 43 | $this->stats = $stats; 44 | } 45 | 46 | public function __invoke(ForumSerializer $serializer, $model, $attributes): array 47 | { 48 | $ttl = (int) $this->settings->get('fof-forum-statistics-widget.cache_duration'); 49 | 50 | $stats = $this->cache->remember(self::CACHE_KEY, $ttl, function (): array { 51 | return $this->buildStats(); 52 | }) ?: []; 53 | 54 | if (empty($stats)) { 55 | return $attributes; 56 | } 57 | 58 | $actor = $serializer->getActor(); 59 | 60 | if ($actor->can('fof-forum-statistics-widget.viewWidget.discussionsCount')) { 61 | $attributes['fof-forum-statistics-widget.discussionsCount'] = Arr::get($stats, 'discussion_count'); 62 | } 63 | 64 | if ($actor->can('fof-forum-statistics-widget.viewWidget.postsCount')) { 65 | $attributes['fof-forum-statistics-widget.postsCount'] = Arr::get($stats, 'comment_post_count'); 66 | } 67 | 68 | if ($actor->can('fof-forum-statistics-widget.viewWidget.usersCount')) { 69 | $attributes['fof-forum-statistics-widget.usersCount'] = Arr::get($stats, 'user_count'); 70 | } 71 | 72 | if ($actor->can('fof-forum-statistics-widget.viewWidget.latestMember')) { 73 | $attributes['fof-forum-statistics-widget.lastUserId'] = Arr::get($stats, 'last_user'); 74 | } 75 | 76 | return $attributes; 77 | } 78 | 79 | protected function buildStats(): array 80 | { 81 | $ignorePrivate = (bool) $this->settings->get('fof-forum-statistics-widget.ignore_private_discussions'); 82 | 83 | return [ 84 | 'discussion_count' => $ignorePrivate ? Discussion::query()->where('is_private', false)->count() : Discussion::query()->count(), 85 | 'user_count' => User::query()->count(), 86 | 'comment_post_count' => CommentPost::query()->count(), 87 | 'last_user' => $this->stats->getLatestUserId(), 88 | ]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Listener/FlushStats.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 34 | $this->settings = $settings; 35 | } 36 | 37 | public function subscribe(Dispatcher $events): void 38 | { 39 | // Flush on both deleting and deleted else a request make in between deleting and deleted will return the old stats. 40 | $events->listen([UserDeleting::class, UserDeleted::class], [$this, 'flushStats']); 41 | 42 | if ((bool) $this->settings->get('fof-forum-statistics-widget.flush_cache_on_new_registration')) { 43 | $events->listen(Registered::class, [$this, 'flushStats']); 44 | } 45 | } 46 | 47 | public function flushStats($event): void 48 | { 49 | $this->cache->forget(AddForumStats::CACHE_KEY); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Repository/StatsRepository.php: -------------------------------------------------------------------------------- 1 | orderBy('joined_at', 'DESC')->limit(1)->first(); 21 | } 22 | 23 | public function getLatestUserId(): ?int 24 | { 25 | return $this->getLatestUser() ? $this->getLatestUser()->id : null; 26 | } 27 | } 28 | --------------------------------------------------------------------------------