├── .editorconfig ├── LICENSE.md ├── 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_03_19_000000_add_post_count_column.php ├── resources ├── less │ ├── admin.less │ └── forum.less └── locale │ └── en.yml └── src ├── Content └── Categories.php └── Util.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.{diff,md}] 16 | trim_trailing_whitespace = false 17 | 18 | [*.{php,xml}] 19 | indent_size = 4 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flarum Categories 2 | 3 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) [![Latest Stable Version](https://img.shields.io/packagist/v/askvortsov/flarum-categories.svg)](https://packagist.org/packages/askvortsov/flarum-categories) 4 | 5 | A [Flarum](http://flarum.org) extension. Ever wanted a traditional categories page for Flarum? Well, now you have it! 6 | 7 | ![On Desktop](https://i.imgur.com/oAOusDE.png) 8 | 9 | ![On Mobile](https://i.imgur.com/HA1DwwC.png) 10 | 11 | Take a look at a site running it at: 12 | https://forum.youthneuro.org 13 | 14 | 15 | ### Installation 16 | 17 | Use [Bazaar](https://discuss.flarum.org/d/5151-flagrow-bazaar-the-extension-marketplace) or install manually with composer: 18 | 19 | ```sh 20 | composer require askvortsov/flarum-categories 21 | ``` 22 | 23 | ### Updating 24 | 25 | ```sh 26 | composer update askvortsov/flarum-categories 27 | ``` 28 | 29 | ### Feedback and Suggestions 30 | 31 | I am very open to feedback and feature suggestions! Please comment on discuss or open a github issue, and I'll consider all feedback. 32 | 33 | ### Links 34 | 35 | - [Packagist](https://packagist.org/packages/askvortsov/flarum-categories) 36 | - [Github](https://github.com/askvortsov1/flarum-categories) 37 | - [Discuss](https://discuss.flarum.org/d/23184-flarum-categories) 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "askvortsov/flarum-categories", 3 | "description": "Traditional Category Layout for Flarum", 4 | "keywords": [ 5 | "flarum" 6 | ], 7 | "type": "flarum-extension", 8 | "license": "MIT", 9 | "support": { 10 | "issues": "https://github.com/askvortsov1/flarum-categories/issues", 11 | "source": "https://github.com/askvortsov1/flarum-categories", 12 | "forum": "https://discuss.flarum.org/d/23184-flarum-categories" 13 | }, 14 | "require": { 15 | "flarum/core": "^1.2.0", 16 | "flarum/tags": "^1.2.0" 17 | }, 18 | "authors": [ 19 | { 20 | "name": "Alexander Skvortsov", 21 | "email": "askvortsov@flarum.org", 22 | "role": "Developer" 23 | } 24 | ], 25 | "autoload": { 26 | "psr-4": { 27 | "Askvortsov\\FlarumCategories\\": "src/" 28 | } 29 | }, 30 | "extra": { 31 | "flarum-extension": { 32 | "title": "Categories", 33 | "category": "theme", 34 | "icon": { 35 | "name": "fas fa-th-list", 36 | "backgroundColor": "#6932D1", 37 | "color": "#fff" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /extend.php: -------------------------------------------------------------------------------- 1 | js(__DIR__.'/js/dist/forum.js') 27 | ->css(__DIR__.'/resources/less/forum.less') 28 | ->route('/categories', 'categories', Categories::class), 29 | 30 | (new Extend\Frontend('admin')) 31 | ->js(__DIR__.'/js/dist/admin.js') 32 | ->css(__DIR__.'/resources/less/admin.less'), 33 | 34 | (new Extend\Settings()) 35 | ->serializeToForum('categories.keepTagsNav', 'askvortsov-categories.keep-tags-nav', 'boolval') 36 | ->serializeToForum('categories.fullPageDesktop', 'askvortsov-categories.full-page-desktop', 'boolval') 37 | ->serializeToForum('categories.compactMobile', 'askvortsov-categories.compact-mobile', 'boolval') 38 | ->serializeToForum('categories.parentRemoveIcon', 'askvortsov-categories.parent-remove-icon', 'boolval') 39 | ->serializeToForum('categories.parentRemoveDescription', 'askvortsov-categories.parent-remove-description', 'boolval') 40 | ->serializeToForum('categories.parentRemoveStats', 'askvortsov-categories.parent-remove-stats', 'boolval') 41 | ->serializeToForum('categories.parentRemoveLastDiscussion', 'askvortsov-categories.parent-remove-last-discussion', 'boolval') 42 | ->serializeToForum('categories.childBareIcon', 'askvortsov-categories.child-bare-icon', 'boolval', true), 43 | 44 | (new Extend\ApiController(ListTagsController::class)) 45 | ->addOptionalInclude('lastPostedDiscussion.lastPostedUser'), 46 | 47 | (new Extend\ApiSerializer(TagSerializer::class)) 48 | ->attributes(function ($serializer, $model, $attributes) { 49 | $settings = resolve(SettingsRepositoryInterface::class); 50 | if ($settings->get('askvortsov-categories.small-forum-optimized', false)) { 51 | $result = $model->discussions() 52 | ->selectRaw('sum(comment_count) as postCount, count(id) as discussionCount') 53 | ->whereVisibleTo($serializer->getActor()) 54 | ->get()[0]; 55 | $attributes['discussionCount'] = (int) $result['discussionCount']; 56 | $attributes['postCount'] = (int) $result['postCount']; 57 | } else { 58 | // discussion count is loaded this way by default, no need to reiterate 59 | $attributes['postCount'] = (int) $model->post_count; 60 | } 61 | 62 | return $attributes; 63 | }), 64 | 65 | (new Extend\ApiSerializer(BasicUserSerializer::class)) 66 | ->attribute('joinTime', function ($serializer, $model) { 67 | return $serializer->formatDate($model->joined_at); 68 | }), 69 | 70 | new Extend\Locales(__DIR__.'/resources/locale'), 71 | 72 | (new Extend\Event()) 73 | ->listen(Hidden::class, function (Hidden $event) { 74 | Util::updateTagsPostCount($event->post, -1); 75 | }) 76 | ->listen(Posted::class, function (Posted $event) { 77 | Util::updateTagsPostCount($event->post, 1); 78 | }) 79 | ->listen(Restored::class, function (Restored $event) { 80 | Util::updateTagsPostCount($event->post, 1); 81 | }), 82 | ]; 83 | -------------------------------------------------------------------------------- /js/admin.js: -------------------------------------------------------------------------------- 1 | export * from './src/admin'; 2 | -------------------------------------------------------------------------------- /js/dist/admin.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 s in a)e.o(a,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:a[s]})},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/extend"],s=flarum.core.compat["admin/components/BasicsPage"];var r=e.n(s);app.initializers.add("askvortsov/flarum-categories",(function(){app.extensionData.for("askvortsov-categories").registerSetting((function(){return m("legend",{class:"categories-legend"},app.translator.trans("askvortsov-categories.admin.headings.nav"))}),10).registerSetting({setting:"askvortsov-categories.keep-tags-nav",label:app.translator.trans("askvortsov-categories.admin.labels.keep_tags_nav"),type:"switch"},9).registerSetting((function(){return m("legend",{class:"categories-legend"},app.translator.trans("askvortsov-categories.admin.headings.layout"))})).registerSetting({setting:"askvortsov-categories.full-page-desktop",label:app.translator.trans("askvortsov-categories.admin.labels.full_page_desktop"),help:app.translator.trans("askvortsov-categories.admin.help.full_page_desktop"),type:"switch"}).registerSetting({setting:"askvortsov-categories.compact-mobile",label:app.translator.trans("askvortsov-categories.admin.labels.compact_mobile_mode"),type:"switch"}).registerSetting((function(){return m("legend",{class:"categories-legend"},app.translator.trans("askvortsov-categories.admin.headings.parent_display"))})).registerSetting({setting:"askvortsov-categories.parent-remove-icon",label:app.translator.trans("askvortsov-categories.admin.labels.parent_remove_icon"),type:"switch"}).registerSetting({setting:"askvortsov-categories.parent-remove-description",label:app.translator.trans("askvortsov-categories.admin.labels.parent_remove_description"),type:"switch"}).registerSetting({setting:"askvortsov-categories.parent-remove-stats",label:app.translator.trans("askvortsov-categories.admin.labels.parent_remove_stats"),type:"switch"}).registerSetting({setting:"askvortsov-categories.parent-remove-last-discussion",label:app.translator.trans("askvortsov-categories.admin.labels.parent_remove_last_discussion"),type:"switch"}).registerSetting((function(){return m("legend",{class:"categories-legend"},app.translator.trans("askvortsov-categories.admin.headings.child_display"))})).registerSetting({setting:"askvortsov-categories.child-bare-icon",label:app.translator.trans("askvortsov-categories.admin.labels.child_bare_icon"),help:app.translator.trans("askvortsov-categories.admin.help.child_bare_icon"),type:"switch"}).registerSetting((function(){return m("legend",{class:"categories-legend"},app.translator.trans("askvortsov-categories.admin.headings.performance"))})).registerSetting({setting:"askvortsov-categories.small-forum-optimized",label:app.translator.trans("askvortsov-categories.admin.labels.small_forum_optimized"),help:app.translator.trans("askvortsov-categories.admin.help.small_forum_optimized"),type:"switch"}),(0,a.extend)(r().prototype,"homePageItems",(function(e){e.add("categories",{path:"/categories",label:app.translator.trans("askvortsov-categories.admin.basics.categories_label")})}))}))})(),module.exports=t})(); 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,IACzBH,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,iBCAlD,EAA+BF,OAAOC,KAAKC,OAAO,+B,aCGxDC,IAAIC,aAAaC,IAAI,gCAAgC,WACnDF,IAAIG,cAAa,IACV,yBACJC,iBAAgB,kBAAMC,EAAA,UAAQC,MAAM,qBAAqBN,IAAIO,WAAWC,MAAM,4CAAqD,GAAE,IACrIJ,gBACC,CACEK,QAAS,sCACTC,MAAOV,IAAIO,WAAWC,MAAM,oDAC5BG,KAAM,UAER,GAEDP,iBAAgB,kBAAMC,EAAA,UAAQC,MAAM,qBAAqBN,IAAIO,WAAWC,MAAM,+CAAwD,IACtIJ,gBAAgB,CACfK,QAAS,0CACTC,MAAOV,IAAIO,WAAWC,MAAM,wDAC5BI,KAAMZ,IAAIO,WAAWC,MAAM,sDAC3BG,KAAM,WAEPP,gBAAgB,CACfK,QAAS,uCACTC,MAAOV,IAAIO,WAAWC,MAAM,0DAC5BG,KAAM,WAEPP,iBAAgB,kBAAMC,EAAA,UAAQC,MAAM,qBAAqBN,IAAIO,WAAWC,MAAM,uDAAgE,IAC9IJ,gBAAgB,CACfK,QAAS,2CACTC,MAAOV,IAAIO,WAAWC,MAAM,yDAC5BG,KAAM,WAEPP,gBAAgB,CACfK,QAAS,kDACTC,MAAOV,IAAIO,WAAWC,MAAM,gEAC5BG,KAAM,WAEPP,gBAAgB,CACfK,QAAS,4CACTC,MAAOV,IAAIO,WAAWC,MAAM,0DAC5BG,KAAM,WAEPP,gBAAgB,CACfK,QAAS,sDACTC,MAAOV,IAAIO,WAAWC,MAAM,oEAC5BG,KAAM,WAEPP,iBAAgB,kBAAMC,EAAA,UAAQC,MAAM,qBAAqBN,IAAIO,WAAWC,MAAM,sDAA+D,IAC7IJ,gBAAgB,CACfK,QAAS,wCACTC,MAAOV,IAAIO,WAAWC,MAAM,sDAC5BI,KAAMZ,IAAIO,WAAWC,MAAM,oDAC3BG,KAAM,WAEPP,iBAAgB,kBAAMC,EAAA,UAAQC,MAAM,qBAAqBN,IAAIO,WAAWC,MAAM,oDAA6D,IAC3IJ,gBAAgB,CACfK,QAAS,8CACTC,MAAOV,IAAIO,WAAWC,MAAM,4DAC5BI,KAAMZ,IAAIO,WAAWC,MAAM,0DAC3BG,KAAM,YAGVE,EAAAA,EAAAA,QAAOC,IAAAA,UAAsB,iBAAiB,SAACC,GAC7CA,EAAMb,IAAI,aAAc,CACtBc,KAAM,cACNN,MAAOV,IAAIO,WAAWC,MAAM,wDAEhC,GACF,G","sources":["webpack://@askvortsov/flarum-categories/webpack/bootstrap","webpack://@askvortsov/flarum-categories/webpack/runtime/compat get default export","webpack://@askvortsov/flarum-categories/webpack/runtime/define property getters","webpack://@askvortsov/flarum-categories/webpack/runtime/hasOwnProperty shorthand","webpack://@askvortsov/flarum-categories/webpack/runtime/make namespace object","webpack://@askvortsov/flarum-categories/external root \"flarum.core.compat['common/extend']\"","webpack://@askvortsov/flarum-categories/external root \"flarum.core.compat['admin/components/BasicsPage']\"","webpack://@askvortsov/flarum-categories/./src/admin/index.js"],"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['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/components/BasicsPage'];","import { extend } from 'flarum/common/extend';\nimport BasicsPage from 'flarum/admin/components/BasicsPage';\n\napp.initializers.add('askvortsov/flarum-categories', () => {\n app.extensionData\n .for('askvortsov-categories')\n .registerSetting(() => {app.translator.trans('askvortsov-categories.admin.headings.nav')}, 10)\n .registerSetting(\n {\n setting: 'askvortsov-categories.keep-tags-nav',\n label: app.translator.trans('askvortsov-categories.admin.labels.keep_tags_nav'),\n type: 'switch',\n },\n 9\n )\n .registerSetting(() => {app.translator.trans('askvortsov-categories.admin.headings.layout')})\n .registerSetting({\n setting: 'askvortsov-categories.full-page-desktop',\n label: app.translator.trans('askvortsov-categories.admin.labels.full_page_desktop'),\n help: app.translator.trans('askvortsov-categories.admin.help.full_page_desktop'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'askvortsov-categories.compact-mobile',\n label: app.translator.trans('askvortsov-categories.admin.labels.compact_mobile_mode'),\n type: 'switch',\n })\n .registerSetting(() => {app.translator.trans('askvortsov-categories.admin.headings.parent_display')})\n .registerSetting({\n setting: 'askvortsov-categories.parent-remove-icon',\n label: app.translator.trans('askvortsov-categories.admin.labels.parent_remove_icon'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'askvortsov-categories.parent-remove-description',\n label: app.translator.trans('askvortsov-categories.admin.labels.parent_remove_description'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'askvortsov-categories.parent-remove-stats',\n label: app.translator.trans('askvortsov-categories.admin.labels.parent_remove_stats'),\n type: 'switch',\n })\n .registerSetting({\n setting: 'askvortsov-categories.parent-remove-last-discussion',\n label: app.translator.trans('askvortsov-categories.admin.labels.parent_remove_last_discussion'),\n type: 'switch',\n })\n .registerSetting(() => {app.translator.trans('askvortsov-categories.admin.headings.child_display')})\n .registerSetting({\n setting: 'askvortsov-categories.child-bare-icon',\n label: app.translator.trans('askvortsov-categories.admin.labels.child_bare_icon'),\n help: app.translator.trans('askvortsov-categories.admin.help.child_bare_icon'),\n type: 'switch',\n })\n .registerSetting(() => {app.translator.trans('askvortsov-categories.admin.headings.performance')})\n .registerSetting({\n setting: 'askvortsov-categories.small-forum-optimized',\n label: app.translator.trans('askvortsov-categories.admin.labels.small_forum_optimized'),\n help: app.translator.trans('askvortsov-categories.admin.help.small_forum_optimized'),\n type: 'switch',\n });\n\n extend(BasicsPage.prototype, 'homePageItems', (items) => {\n items.add('categories', {\n path: '/categories',\n label: app.translator.trans('askvortsov-categories.admin.basics.categories_label'),\n });\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","app","initializers","add","extensionData","registerSetting","m","class","translator","trans","setting","label","type","help","extend","BasicsPage","items","path"],"sourceRoot":""} -------------------------------------------------------------------------------- /js/dist/forum.js: -------------------------------------------------------------------------------- 1 | (()=>{var t={n:a=>{var o=a&&a.__esModule?()=>a.default:()=>a;return t.d(o,{a:o}),o},d:(a,o)=>{for(var e in o)t.o(o,e)&&!t.o(a,e)&&Object.defineProperty(a,e,{enumerable:!0,get:o[e]})},o:(t,a)=>Object.prototype.hasOwnProperty.call(t,a),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},a={};(()=>{"use strict";t.r(a),t.d(a,{default:()=>at});const o=flarum.core.compat["common/extend"],e=flarum.core.compat["forum/components/IndexPage"];var s=t.n(e);const r=flarum.core.compat["common/components/LinkButton"];var n=t.n(r);const i=flarum.core.compat["common/Model"];var c=t.n(i);const l=flarum.core.compat["tags/models/Tag"];var u=t.n(l);function d(t,a){return d=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,a){return t.__proto__=a,t},d(t,a)}function g(t,a){t.prototype=Object.create(a.prototype),t.prototype.constructor=t,d(t,a)}const p=flarum.core.compat["forum/app"];var h=t.n(p);const f=flarum.core.compat["common/components/Page"];var v=t.n(f);const y=flarum.core.compat["common/components/LoadingIndicator"];var C=t.n(y);const b=flarum.core.compat["common/helpers/listItems"];var w=t.n(b);const T=flarum.core.compat["common/utils/ItemList"];var k=t.n(T);const N=flarum.core.compat["common/utils/extractText"];var P=t.n(N);const I=flarum.core.compat["common/utils/classList"];var L=t.n(I);const D=flarum.core.compat["tags/utils/sortTags"];var A=t.n(D);const M=flarum.core.compat["tags/helpers/tagLabel"];var x=t.n(M);const _=flarum.core.compat["common/Component"];var S=t.n(_);const O=flarum.core.compat["common/components/Link"];var U=t.n(O);const B=flarum.core.compat["common/helpers/icon"];var j=t.n(B),V=function(t){function a(){return t.apply(this,arguments)||this}g(a,t);var o=a.prototype;return o.view=function(){return m("div",{class:L()("StatWidget",{"Categories-compactMobileModeEnabled":!!h().forum.attribute("categories.compactMobile")})},this.content().toArray())},o.content=function(){var t=new(k());return t.add("count",m("div",{class:"StatWidget-count"},this.attrs.count),100),t.add("label",m("div",{class:"StatWidget-label"},m("span",{className:"Categories-showOnMobile"},j()(this.attrs.icon)),m("span",{className:"Categories-hideOnMobile"},this.attrs.label)),80),t},a}(S());const W=flarum.core.compat["common/helpers/avatar"];var E=t.n(W);const R=flarum.core.compat["common/helpers/username"];var F=t.n(R);const z=flarum.core.compat["common/helpers/humanTime"];var G=t.n(z);const q=flarum.core.compat["common/utils/string"],H=flarum.core.compat["forum/components/UserCard"];var J=t.n(H),K=function(t){function a(){for(var a,o=arguments.length,e=new Array(o),s=0;s {\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['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/IndexPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LinkButton'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Model'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['tags/models/Tag'];","export default function _setPrototypeOf(o, p) {\n _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {\n o.__proto__ = p;\n return o;\n };\n return _setPrototypeOf(o, p);\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}","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Page'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LoadingIndicator'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/listItems'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/ItemList'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/extractText'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/classList'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['tags/utils/sortTags'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['tags/helpers/tagLabel'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Component'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Link'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/icon'];","import Component from 'flarum/common/Component';\nimport icon from 'flarum/common/helpers/icon';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport classList from 'flarum/common/utils/classList';\nimport app from 'flarum/forum/app';\n\nimport type Mithril from 'mithril';\n\ninterface Attrs {\n count: number;\n icon: string;\n label: Mithril.Children;\n}\n\nexport default class StatWidget extends Component {\n view() {\n return (\n
\n {this.content().toArray()}\n
\n );\n }\n\n content() {\n const items = new ItemList();\n\n items.add('count',
{this.attrs.count}
, 100);\n items.add(\n 'label',\n
\n {icon(this.attrs.icon)}\n {this.attrs.label}\n
,\n 80\n );\n\n return items;\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/avatar'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/username'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/humanTime'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/string'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/UserCard'];","import app from 'flarum/forum/app';\nimport Component from 'flarum/common/Component';\nimport avatar from 'flarum/common/helpers/avatar';\nimport username from 'flarum/common/helpers/username';\nimport humanTime from 'flarum/common/helpers/humanTime';\nimport { truncate } from 'flarum/common/utils/string';\nimport Link from 'flarum/common/components/Link';\nimport UserCard from 'flarum/forum/components/UserCard';\nimport type Discussion from 'flarum/common/models/Discussion';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport extractText from 'flarum/common/utils/extractText';\nimport type Mithril from 'mithril';\n\ninterface Attrs {\n discussion: Discussion;\n}\n\nexport default class LastDiscussionWidget extends Component {\n /**\n * Whether or not the user hover card is visible.\n */\n cardVisible!: boolean;\n\n oninit(vnode) {\n super.oninit(vnode);\n\n this.cardVisible = false;\n }\n\n view() {\n const discussion = this.attrs.discussion;\n\n if (!discussion) {\n return
{app.translator.trans('askvortsov-categories.forum.last_discussion_widget.no_discussions')}
;\n }\n\n const user = discussion.lastPostedUser();\n\n return (\n \n {this.content().toArray()}\n \n );\n }\n\n content() {\n const items = new ItemList();\n\n const discussion = this.attrs.discussion;\n const user = discussion.lastPostedUser();\n\n items.add(\n 'avatar',\n \n {!!user && avatar(user)}\n ,\n 100\n );\n\n items.add(\n 'mainContent',\n
\n
\n {humanTime(discussion.lastPostedAt()!)}{' '}\n \n \n |\n \n {username(user)}\n \n
\n
{truncate(discussion.title(), 26)}
\n
,\n 50\n );\n\n let card: Mithril.Children = null;\n\n if (user && this.cardVisible) {\n card = UserCard.component({\n user,\n className: 'UserCard--popover',\n controlsButtonClassName: 'Button Button--icon Button--flat',\n });\n }\n\n items.add('card',
{card}
, 10);\n\n return items;\n }\n\n oncreate(vnode) {\n super.oncreate(vnode);\n\n let timeout: number;\n\n this.$()\n .on('mouseover', '.LastDiscussion-avatar, .LastDiscussion-usernameLinkUserCard, .username, .UserCard', () => {\n clearTimeout(timeout);\n timeout = window.setTimeout(this.showCard.bind(this), 300);\n })\n .on('mouseout', '.LastDiscussion-avatar, .LastDiscussion-usernameLinkUserCard, .username, .UserCard', () => {\n clearTimeout(timeout);\n timeout = window.setTimeout(this.hideCard.bind(this), 150);\n });\n }\n\n onremove(vnode): void {\n super.onremove(vnode);\n\n this.$().off('mouseover mouseout');\n }\n\n /**\n * Show the user card.\n */\n showCard() {\n this.cardVisible = true;\n\n m.redraw();\n\n setTimeout(() => this.$('.UserCard').addClass('in'));\n }\n\n /**\n * Hide the user card.\n */\n hideCard() {\n $('.UserCard').removeClass('in');\n this.cardVisible = false;\n m.redraw();\n }\n}\n","import Component from 'flarum/common/Component';\nimport Link from 'flarum/common/components/Link';\n\nimport icon from 'flarum/common/helpers/icon';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport sortTags from 'flarum/tags/utils/sortTags';\n\nimport StatWidget from './StatWidget';\nimport LastDiscussionWidget from './LastDiscussionWidget';\nimport app from 'flarum/forum/app';\nimport classList from 'flarum/common/utils/classList';\n\nimport type Mithril from 'mithril';\n\ninterface Attrs {\n model: any;\n parent: any;\n}\n\n/*\n * Provide compatibility for Flarum Tag Passwords extension, this is to ensure that locked tag with Password or Group permission has view restriction on latest discussion.\n * To know if a tag is protected, there is an isUnlocked variable that is specific for this extension.\n * https://github.com/datlechin/flarum-tag-passwords\n * \n */\ninterface TagLocked {\n icon: icon;\n text: string;\n isVisible: boolean;\n}\n\nexport default class Category extends Component {\n tag!: any;\n isChild!: boolean;\n collapsed!: boolean;\n compactMobileMode!: boolean;\n tagLocked!: TagLocked;\n\n oninit(vnode) {\n super.oninit(vnode);\n\n this.tag = this.attrs.model;\n\n this.isChild = this.attrs.parent != null && this.attrs.parent != undefined;\n\n this.collapsed = false;\n\n // Identify if the tag has been Goup or Password protected with flarum-tag-passwords extension.\n if (typeof this.tag.isUnlocked == 'function') {\n if ((this.tag.isGroupProtected() || this.tag.isPasswordProtected()) && !this.tag.isUnlocked()) {\n this.tagLocked = {\n icon: this.tag.isPasswordProtected() ? icon('fas fa-lock') : icon('fas fa-user-lock'),\n text: this.tag.isPasswordProtected() ? app.translator.trans('datlechin-tag-passwords.forum.tags_page.password_protected') : app.translator.trans('datlechin-tag-passwords.forum.tags_page.group_protected'),\n isVisible: this.tag.isProtectedTagDisplayedForTagsPage() == true\n };\n }\n }\n\n window.addEventListener('resize', function () {\n m.redraw();\n });\n }\n\n view() {\n const tag = this.tag;\n\n if (!tag) {\n return null;\n } else if (this.tagLocked && !this.tagLocked.isVisible) {\n // Hide the navigation when protected Tag in 'Tag Passwords > Display protected Tag in Tags page navigation' is disabled\n return null;\n }\n\n this.compactMobileMode = !!app.forum.attribute('categories.compactMobile');\n\n return (\n \n {this.categoryItems().toArray()}\n \n );\n }\n\n categoryItems() {\n const items = new ItemList();\n const tag = this.tag;\n\n const children = this.isChild ? [] : sortTags(tag.children() || []);\n\n items.add(\n 'link',\n \n {this.contentItems().toArray()}\n ,\n 100\n );\n\n if (!this.compactMobileMode && !this.isChild) {\n items.add(\n 'children',\n
    {children.map((child) => [Category.component({ model: child, parent: this })])}
,\n 10\n );\n }\n\n return items;\n }\n\n contentItems() {\n const items = new ItemList();\n\n const tag = this.tag;\n const children = this.isChild ? [] : sortTags(tag.children() || []);\n\n items.add('alignStart',
{this.alignStartItems().toArray()}
, 100);\n\n items.add('alignEnd',
{this.alignEndItems().toArray()}
, 50);\n\n const childrenInContent = !this.isChild && this.compactMobileMode;\n\n if (childrenInContent && !this.collapsed) {\n items.add(\n 'children',\n
    {children.map((child) => [Category.component({ model: child, parent: this })])}
,\n 10\n );\n }\n\n return items;\n }\n\n alignStartItems() {\n const items = new ItemList();\n\n const tag = this.tag;\n const children = this.isChild ? [] : sortTags(tag.children() || []);\n\n items.add('icon', {this.iconItems().toArray()}, 100);\n\n items.add('main',
{this.mainItems().toArray()}
, 50);\n\n if (!!children.length) {\n items.add(\n 'toggleArrow',\n {\n this.toggleArrow(e);\n }}\n >\n {icon(this.collapsed ? 'fas fa-caret-down' : 'fas fa-caret-up')}\n ,\n 10\n );\n }\n\n return items;\n }\n\n alignEndItems() {\n const items = new ItemList();\n\n if (this.tagLocked) {\n items.add('locked',
{this.lockedItems().toArray()}
, 100);\n return items;\n }\n\n const tag = this.tag;\n\n items.add('stats',
{this.statItems().toArray()}
, 100);\n\n items.add(\n 'lastDiscussion',\n
{this.lastDiscussionItems().toArray()}
,\n 50\n );\n\n return items;\n }\n\n lockedItems() {\n const items = new ItemList();\n const classes = this.compactMobileMode ? 'fa-stack fa-1x' : 'fa-stack fa-2x';\n items.add(\n 'icon',\n \n {\n \n }\n {this.tagLocked.icon}\n ,\n 10\n );\n items.add(\n 'LockedText',\n
{this.tagLocked.text}
,\n 50\n );\n return items;\n }\n\n iconItems() {\n const items = new ItemList();\n\n if (this.tag.icon() && this.isChild) {\n const style: Record = {};\n\n let iconClasses = 'fa-stack-1x CategoryIcon';\n\n if (app.forum.attribute('categories.childBareIcon')) {\n iconClasses += ' NoBackgroundCategoryIcon';\n style.color = '#fafafa';\n } else {\n style.color = this.tag.color();\n }\n\n const classes = this.compactMobileMode ? 'fa-stack fa-1x' : 'fa-stack fa-2x';\n\n items.add(\n 'icon',\n \n {!!app.forum.attribute('categories.childBareIcon') && (\n \n )}\n {icon(this.tag.icon(), { className: iconClasses, style: style })}\n ,\n 10\n );\n } else if (this.tag.icon() && !app.forum.attribute('categories.parentRemoveIcon')) {\n const classes = this.compactMobileMode ? 'fa-stack fa-2x' : 'fa-stack fa-3x';\n\n items.add('icon', {icon(this.tag.icon(), { className: 'fa-stack-1x CategoryIcon' })}, 10);\n }\n\n return items;\n }\n\n mainItems() {\n const items = new ItemList();\n\n items.add('name',

{this.tag.name()}

, 15);\n\n if (this.tag.description() && (this.isChild || !app.forum.attribute('categories.parentRemoveDescription'))) {\n items.add('description',
{this.tag.description()}
, 10);\n }\n\n return items;\n }\n\n statItems() {\n const items = new ItemList();\n\n if (this.isChild || !app.forum.attribute('categories.parentRemoveStats')) {\n items.add(\n 'discussionCount',\n StatWidget.component({\n count: Intl.NumberFormat().format(this.tag.discussionCount()),\n label: app.translator.trans('askvortsov-categories.forum.stat-widgets.discussion_label'),\n icon: 'fas fa-file-alt',\n }),\n 15\n );\n\n items.add(\n 'postCount',\n StatWidget.component({\n count: Intl.NumberFormat().format(this.tag.postCount()),\n label: app.translator.trans('askvortsov-categories.forum.stat-widgets.post_label'),\n icon: 'fas fa-comment',\n }),\n 10\n );\n }\n\n return items;\n }\n\n lastDiscussionItems() {\n const items = new ItemList();\n\n if (this.isChild || !app.forum.attribute('categories.parentRemoveLastDiscussion')) {\n items.add(\n 'lastDiscussion',\n LastDiscussionWidget.component({\n discussion: this.tag.lastPostedDiscussion(),\n }),\n 10\n );\n }\n\n return items;\n }\n\n oncreate(vnode) {\n super.oncreate(vnode);\n\n this.$('.TagCategory-content,.TagCategory-toggleArrow').on('mouseenter', function (e) {\n $(this).addClass('hover');\n if ($(this).parent().hasClass('SubCategory') || $(this).hasClass('TagCategory-toggleArrow')) {\n $(this).closest('.ParentCategory').children('.TagCategory-content').removeClass('hover');\n }\n });\n\n this.$('.TagCategory-content,.TagCategory-toggleArrow').on('mouseleave', function (e) {\n $(this).removeClass('hover');\n if ($(this).parent().hasClass('SubCategory') || $(this).hasClass('TagCategory-toggleArrow')) {\n $(this).closest('.ParentCategory').children('.TagCategory-content').addClass('hover');\n }\n });\n }\n\n onremove(vnode: Mithril.VnodeDOM): void {\n super.onremove(vnode);\n\n this.$('.TagCategory-content,.TagCategory-toggleArrow').off('mouseenter');\n this.$('.TagCategory-content,.TagCategory-toggleArrow').off('mouseleave');\n }\n\n toggleArrow(e: MouseEvent) {\n e.preventDefault();\n e.stopPropagation();\n this.collapsed = !this.collapsed;\n m.redraw();\n }\n}\n","import app from 'flarum/forum/app';\nimport Page from 'flarum/common/components/Page';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\nimport IndexPage from 'flarum/forum/components/IndexPage';\nimport listItems from 'flarum/common/helpers/listItems';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport extractText from 'flarum/common/utils/extractText';\nimport classList from 'flarum/common/utils/classList';\n\nimport sortTags from 'flarum/tags/utils/sortTags';\nimport tagLabel from 'flarum/tags/helpers/tagLabel';\n\nimport Category from './Category';\n\nexport default class CategoriesPage extends Page {\n tags!: any[];\n loading!: boolean;\n\n oninit(vnode) {\n super.oninit(vnode);\n\n app.history.push('categories', extractText(app.translator.trans('askvortsov-category.forum.header.back_to_categories_tooltip')));\n\n this.tags = [];\n\n const preloaded = app.preloadedApiDocument();\n\n if (preloaded) {\n this.tags = sortTags(preloaded.filter((tag: any) => !tag.isChild()));\n return;\n }\n\n this.loading = true;\n\n app.tagList.load(['parent', 'children', 'lastPostedDiscussion', 'lastPostedDiscussion.lastPostedUser']).then(() => {\n this.tags = sortTags(app.store.all('tags').filter((tag) => !tag.isChild()));\n\n this.loading = false;\n\n m.redraw();\n });\n }\n\n view() {\n if (this.loading) {\n return ;\n }\n\n const classes = ['CategoriesPage'];\n\n return
{this.pageItems().toArray()}
;\n }\n\n pageItems() {\n const items = new ItemList();\n\n items.add('hero', IndexPage.prototype.hero(), 100);\n\n items.add(\n 'container',\n
\n {this.containerItems().toArray()}\n
,\n 50\n );\n\n return items;\n }\n\n containerItems() {\n const items = new ItemList();\n\n const pinned = this.tags.filter((tag) => tag.position() !== null);\n const cloud = this.tags.filter((tag) => tag.position() === null);\n\n items.add(\n 'sideNav',\n ,\n 100\n );\n\n items.add(\n 'categoriesList',\n
\n
    \n {pinned.map((tag) => {\n return Category.component({ model: tag });\n })}\n
\n\n {cloud.length ?
{cloud.map((tag) => [tagLabel(tag, { link: true }), ' '])}
: ''}\n
,\n 50\n );\n\n return items;\n }\n\n oncreate(vnode) {\n super.oncreate(vnode);\n\n app.setTitle(extractText(app.translator.trans('askvortsov-categories.forum.all_categories.meta_title_text')));\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['tags/forum/components/TagsPage'];","import { extend } from 'flarum/common/extend';\nimport IndexPage from 'flarum/forum/components/IndexPage';\nimport LinkButton from 'flarum/common/components/LinkButton';\nimport Model from 'flarum/common/Model';\nimport Tag from 'flarum/tags/models/Tag';\nimport CategoriesPage from './components/CategoriesPage';\nimport TagsPage from 'flarum/tags/forum/components/TagsPage';\nimport Category from './components/Category';\nimport LastDiscussionWidget from './components/LastDiscussionWidget';\nimport StatWidget from './components/StatWidget';\n\nfunction pruneIndexNav(items, func) {\n const isTagsPageVisible = app.forum.attribute('categories.keepTagsNav');\n\n const isCustomTagsHidden = (app.current.matches(CategoriesPage) || app.current.matches(TagsPage));\n for (const item in items.items) {\n if (func(item)) {\n if(item == 'tags') {\n /*\n * Tags must be visible on the navibation bar, when the User has selected to keep Tags within the Extension Settings.\n * Finding all items that begins with 'tag' will also load 'tags', due to custom tag are labelled 'tag1', 'tag2' and so-on\n */\n if (!isTagsPageVisible) {\n items.remove(item);\n }\n } else {\n /*\n * This is for custom tags, where they should not be visible within CategoriesPage and TagsPage\n */\n if (isCustomTagsHidden) {\n items.remove(item);\n }\n }\n }\n }\n}\n\napp.initializers.add('askvortsov/flarum-categories', () => {\n app.routes.categories = {\n path: '/categories',\n component: CategoriesPage,\n };\n\n Tag.prototype.postCount = Model.attribute('postCount');\n\n extend(IndexPage.prototype, 'navItems', function (items) {\n items.add(\n 'categories',\n \n {app.translator.trans('askvortsov-categories.forum.index.categories_link')}\n ,\n -9.5\n );\n\n if (items.has('moreTags')) {\n items.replace('moreTags', {app.translator.trans('flarum-tags.forum.index.more_link')});\n }\n\n pruneIndexNav(items, (item) => item.startsWith('tag'));\n\n return items;\n });\n\n extend(IndexPage.prototype, 'sidebarItems', function (items) {\n pruneIndexNav(items, (item) => item !== 'newDiscussion' && item !== 'nav');\n return items;\n });\n});\n\nexport default {\n 'components/CategoriesPage': CategoriesPage,\n 'components/Category': Category,\n 'components/LastDiscussionWidget': LastDiscussionWidget,\n 'components/StatWidget': StatWidget,\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","bind","__proto__","_inheritsLoose","subClass","superClass","create","constructor","StatWidget","_Component","apply","arguments","_proto","view","m","class","classList","app","this","content","toArray","items","ItemList","add","attrs","count","className","icon","label","Component","LastDiscussionWidget","_this","_len","length","args","Array","_key","concat","cardVisible","oninit","vnode","_discussion$lastPostN","discussion","lastPostedUser","Link","href","lastPostNumber","user","extractText","username","avatar","humanTime","lastPostedAt","style","display","margin","role","truncate","title","card","UserCard","controlsButtonClassName","oncreate","timeout","_this2","$","on","clearTimeout","window","setTimeout","showCard","hideCard","onremove","off","_this3","redraw","addClass","removeClass","Category","tag","isChild","collapsed","compactMobileMode","tagLocked","model","parent","undefined","isUnlocked","isGroupProtected","isPasswordProtected","text","isVisible","isProtectedTagDisplayedForTagsPage","addEventListener","slug","SubCategory","ParentCategory","compactMobile","categoryItems","children","sortTags","backgroundColor","color","contentItems","map","child","component","alignStartItems","alignEndItems","_this4","iconItems","mainItems","onclick","e","toggleArrow","lockedItems","statItems","empty","lastPostedDiscussion","lastDiscussionItems","classes","iconClasses","name","description","Intl","NumberFormat","format","discussionCount","postCount","hasClass","closest","preventDefault","stopPropagation","CategoriesPage","_Page","tags","loading","preloaded","filter","then","LoadingIndicator","pageItems","IndexPage","containerItems","pinned","position","cloud","listItems","tagLabel","link","Page","pruneIndexNav","func","isTagsPageVisible","forum","attribute","isCustomTagsHidden","current","matches","TagsPage","item","remove","initializers","routes","categories","path","Tag","Model","extend","LinkButton","route","translator","trans","has","replace","startsWith"],"sourceRoot":""} -------------------------------------------------------------------------------- /js/forum.js: -------------------------------------------------------------------------------- 1 | export { default } from "./src/forum"; 2 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@askvortsov/flarum-categories", 3 | "version": "0.0.0", 4 | "private": true, 5 | "prettier": "@flarum/prettier-config", 6 | "dependencies": { 7 | "flarum-webpack-config": "^2.0.0", 8 | "webpack": "^5.75.0", 9 | "webpack-cli": "^5.0.1" 10 | }, 11 | "devDependencies": { 12 | "husky": "^4.3.8", 13 | "prettier": "2.8.4", 14 | "@flarum/prettier-config": "^1.0.0", 15 | "flarum-tsconfig": "^1.0.2" 16 | }, 17 | "scripts": { 18 | "dev": "webpack --mode development --watch", 19 | "build": "webpack --mode production", 20 | "format": "prettier --write src", 21 | "format-check": "prettier --check src" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "npm run format" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | "allowJs": true, 10 | // This will output typings to `dist-typings` 11 | "declarationDir": "./dist-typings", 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_03_19_000000_add_post_count_column.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->table('tags', function (Blueprint $table) { 18 | $table->integer('post_count')->unsigned()->default(0); 19 | }); 20 | }, 21 | 22 | 'down' => function (Builder $schema) { 23 | $schema->table('tags', function (Blueprint $table) { 24 | $table->dropColumn('post_count'); 25 | }); 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /resources/less/admin.less: -------------------------------------------------------------------------------- 1 | .categories-legend { 2 | font-size: 16px; 3 | margin-top: 10px; 4 | } 5 | .categories-label { 6 | font-weight: normal; 7 | } -------------------------------------------------------------------------------- /resources/less/forum.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | @categories-color-white: darken(#fff, 2%); 4 | @categories-color-white-slightly-muted: darken(@categories-color-white, 5%); 5 | @categories-color-white-muted: darken(@categories-color-white, 11%); 6 | @categories-color-white-more-muted: darken(@categories-color-white, 25%); 7 | 8 | @categories-color-highlighted: @categories-color-white; 9 | @categories-color-muted: @categories-color-white-muted; 10 | 11 | // Dark colors (for nightmode workaround) 12 | @categories-sub-dark-color-background: lighten(#000, 20%); 13 | @categories-sub-dark-color-background-hover: lighten(#000, 30%); 14 | @categories-sub-dark-color-highlighted: darken(@categories-color-white, 5%); 15 | @categories-sub-dark-color-muted: darken(@categories-color-white, 11%); 16 | @categories-sub-dark-color-more-muted: darken(@categories-color-white, 25%); 17 | 18 | .define-colors(@config-dark-mode); 19 | .define-colors(false) { 20 | @categories-sub-color-background: darken(#fff, 10%); 21 | @categories-sub-color-background-hover: darken(#fff, 15%); 22 | @categories-sub-color-highlighted: lighten(#000, 5%); 23 | @categories-sub-color-muted: lighten(@categories-sub-color-highlighted, 30%); 24 | @categories-sub-color-more-muted: lighten( 25 | @categories-sub-color-highlighted, 26 | 75% 27 | ); 28 | } 29 | .define-colors(true) { 30 | @categories-sub-color-background: @categories-sub-dark-color-background; 31 | @categories-sub-color-background-hover: @categories-sub-dark-color-background-hover; 32 | @categories-sub-color-highlighted: @categories-sub-dark-color-highlighted; 33 | @categories-sub-color-muted: @categories-sub-dark-color-muted; 34 | @categories-sub-color-more-muted: @categories-sub-dark-color-more-muted; 35 | } 36 | 37 | .Categories-showOnMobile { 38 | display: none; 39 | } 40 | 41 | .Categories-compactMobileModeEnabled { 42 | @media @phone { 43 | .Categories-showOnMobile { 44 | display: unset; 45 | } 46 | 47 | .Categories-hideOnMobile { 48 | display: none; 49 | } 50 | } 51 | } 52 | 53 | /* Recreate sideNav but keep horizontal styling */ 54 | .topNav>ul { 55 | margin: 0; 56 | padding: 0; 57 | list-style: none 58 | } 59 | 60 | @media (min-width: 768px) { 61 | .topNav .Dropdown--select { 62 | display:block 63 | } 64 | 65 | .topNav .Dropdown--select .Dropdown-toggle { 66 | display: none 67 | } 68 | 69 | .topNav .Dropdown--select .Dropdown-menu { 70 | display: block; 71 | border: 0; 72 | width: auto; 73 | margin: 0; 74 | padding: 0; 75 | min-width: 0; 76 | float: none; 77 | position: static; 78 | background: none; 79 | box-shadow: none 80 | } 81 | 82 | .topNav .Dropdown--select .Dropdown-menu>li>a { 83 | padding: 8px 0 8px 30px; 84 | color: var(--topNav-color, var(--muted-color)) 85 | } 86 | 87 | .topNav .Dropdown--select .Dropdown-menu>li>a:hover { 88 | background: none; 89 | color: var(--topNav-color-hover, var(--link-color)); 90 | text-decoration: none 91 | } 92 | 93 | .topNav .Dropdown--select .Dropdown-menu>li>a .Button-icon { 94 | float: left; 95 | margin-left: -30px; 96 | font-size: 15px 97 | } 98 | 99 | .topNav .Dropdown--select .Dropdown-menu>li.active>a { 100 | background: none; 101 | color: var(--topNav-color-active, var(--primary-color)); 102 | font-weight: bold 103 | } 104 | 105 | .topNav .Dropdown--select .Dropdown-menu>.Dropdown-separator { 106 | background: none 107 | } 108 | } 109 | @media (min-width: 768px) and (max-width: 991.98px) { 110 | .topNav { 111 | padding:15px 0; 112 | white-space: nowrap; 113 | overflow: auto; 114 | -webkit-overflow-scrolling: touch 115 | } 116 | 117 | .topNav:after { 118 | content: " "; 119 | position: absolute; 120 | left: 0; 121 | right: 0; 122 | margin-top: 15px; 123 | border-bottom: 1px solid var(--control-bg) 124 | } 125 | 126 | .topNav>ul>li,.topNav .Dropdown-menu>li { 127 | display: inline-block; 128 | margin: 0 20px 0 0; 129 | vertical-align: top 130 | } 131 | 132 | .topNav .Dropdown-separator { 133 | display: none 134 | } 135 | 136 | .topNav .Dropdown--select .Dropdown-menu>li>a { 137 | padding-left: 25px 138 | } 139 | 140 | .topNav .Dropdown--select .Dropdown-menu>li>a .icon { 141 | margin-left: -25px 142 | } 143 | 144 | .topNav .affix { 145 | position: static 146 | } 147 | } 148 | 149 | @media (min-width: 992px) { 150 | .CategoriesPage .topNav { 151 | padding:15px 0; 152 | white-space: nowrap; 153 | overflow: auto; 154 | -webkit-overflow-scrolling: touch; 155 | float: none; 156 | width: auto; 157 | padding-top: 30px 158 | } 159 | 160 | .CategoriesPage .topNav:after { 161 | content: " "; 162 | position: absolute; 163 | left: 0; 164 | right: 0; 165 | margin-top: 15px; 166 | border-bottom: 1px solid var(--control-bg) 167 | } 168 | 169 | .CategoriesPage .topNav>ul>li,.CategoriesPage .topNav .Dropdown-menu>li { 170 | display: inline-block; 171 | margin: 0 20px 0 0; 172 | vertical-align: top 173 | } 174 | 175 | .CategoriesPage .topNav .Dropdown-separator { 176 | display: none 177 | } 178 | 179 | .CategoriesPage .topNav .Dropdown--select .Dropdown-menu>li>a { 180 | padding-left: 25px 181 | } 182 | 183 | .CategoriesPage .topNav .Dropdown--select .Dropdown-menu>li>a .icon { 184 | margin-left: -25px 185 | } 186 | 187 | .CategoriesPage .topNav .affix { 188 | position: static 189 | } 190 | 191 | .CategoriesPage .topNav:after { 192 | display: none 193 | } 194 | 195 | .CategoriesPage .topNav>ul>li:first-child { 196 | width: 190px 197 | } 198 | .CategoriesPage .topNavOffset { 199 | margin: 15px 0 0 200 | } 201 | } 202 | 203 | .topNav--horizontal { 204 | padding: 15px 0; 205 | white-space: nowrap; 206 | overflow: auto; 207 | -webkit-overflow-scrolling: touch; 208 | } 209 | 210 | .topNav--horizontal:after { 211 | content: " "; 212 | position: absolute; 213 | left: 0; 214 | right: 0; 215 | margin-top: 15px; 216 | border-bottom: 1px solid var(--control-bg) 217 | } 218 | 219 | .topNav--horizontal>ul>li,.topNav--horizontal .Dropdown-menu>li { 220 | display: inline-block; 221 | margin: 0 20px 0 0; 222 | vertical-align: top 223 | } 224 | 225 | .topNav--horizontal .Dropdown-separator { 226 | display: none 227 | } 228 | 229 | .topNav--horizontal .Dropdown--select .Dropdown-menu>li>a { 230 | padding-left: 25px 231 | } 232 | 233 | .topNav--horizontal .Dropdown--select .Dropdown-menu>li>a .icon { 234 | margin-left: -25px 235 | } 236 | 237 | .topNav--horizontal .affix { 238 | position: static 239 | } 240 | .active>.TagLinkButton { 241 | --topNav-color-active: var(--color) 242 | } 243 | 244 | // Systems 245 | .TagCategory { 246 | list-style: none; 247 | color: @categories-color-white; 248 | 249 | .TagCategory-content { 250 | padding: 15px 20px; 251 | border-radius: 6px; 252 | display: flex; 253 | justify-content: space-between; 254 | background-color: @primary-color; 255 | text-decoration: none !important; 256 | color: @categories-color-highlighted; 257 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.18), 258 | 0 3px 10px 0 rgba(0, 0, 0, 0.15); 259 | 260 | .TagCategory-alignStart, 261 | .TagCategory-alignStart-main, 262 | .TagCategory-alignEnd { 263 | display: flex; 264 | align-items: center; 265 | } 266 | 267 | .TagCategory-toggleArrow { 268 | display: none; 269 | } 270 | 271 | .TagCategory-icon, 272 | .TagCategory-main, 273 | .TagCategory-stats, 274 | .TagCategory-locked, 275 | .TagCategory-lastDiscussion { 276 | display: flex; 277 | } 278 | 279 | .TagCategory-locked { 280 | width: 100%; 281 | justify-content: space-between; 282 | } 283 | 284 | .TagCategory-icon, 285 | .TagCategory-stats, 286 | .TagCategory-locked, 287 | .TagCategory-lastDiscussion { 288 | align-items: center; 289 | } 290 | 291 | .TagCategory-main { 292 | flex-direction: column; 293 | text-align: left; 294 | justify-content: center; 295 | overflow: hidden; 296 | 297 | .TagCategory-name { 298 | display: block; 299 | margin: 0; 300 | font-size: 20px; 301 | } 302 | 303 | .TagCategory-description { 304 | margin-top: 0.5em; 305 | color: @categories-color-muted; 306 | overflow-wrap: break-word; 307 | } 308 | } 309 | 310 | &.hover:hover { 311 | opacity: 90%; 312 | } 313 | } 314 | } 315 | 316 | .TagCategoryList { 317 | padding-left: 0; 318 | margin-top: 0; 319 | 320 | > .TagCategory { 321 | margin-bottom: 25px; 322 | } 323 | } 324 | 325 | .TagCategory.SubCategory { 326 | .TagCategory-content { 327 | color: @categories-sub-color-highlighted; 328 | padding: 10px 20px; 329 | margin: 5px 0; 330 | background-color: @categories-sub-color-background; 331 | box-shadow: 0 2px 4px 0em rgba(0, 0, 0, 0.04), 332 | 0 3px 10px 0 rgba(0, 0, 0, 0.07); 333 | 334 | .TagCategory-main .TagCategory-description { 335 | color: @categories-sub-color-muted; 336 | } 337 | 338 | .TagCategory-name { 339 | font-size: 18px; 340 | } 341 | 342 | .TagCategory-icon { 343 | .CategoryIcon { 344 | color: @categories-sub-color-background; 345 | } 346 | 347 | .CategoryIcon.NoBackgroundCategoryIcon { 348 | color: @categories-sub-color-highlighted; 349 | } 350 | } 351 | 352 | &.hover:hover { 353 | background-color: @categories-sub-color-background-hover; 354 | } 355 | } 356 | 357 | &:first-child { 358 | margin-top: 10px; 359 | } 360 | &:last-child { 361 | border-bottom: none; 362 | } 363 | } 364 | 365 | .StatWidgetList { 366 | display: flex; 367 | 368 | > .StatWidget { 369 | border-right: 2px solid @categories-color-white-more-muted; 370 | display: flex; 371 | padding: 0 10px; 372 | align-items: center; 373 | 374 | > div { 375 | display: flex; 376 | } 377 | 378 | &:last-child { 379 | border-right: none !important; 380 | } 381 | 382 | .StatWidget-count { 383 | font-weight: 550; 384 | } 385 | 386 | .StatWidget-label { 387 | color: @categories-color-white-muted; 388 | } 389 | } 390 | } 391 | 392 | .TagCategory.SubCategory { 393 | .StatWidget { 394 | border-right: 2px solid @categories-sub-color-more-muted; 395 | display: flex; 396 | } 397 | .StatWidget-label { 398 | color: @categories-sub-color-muted; 399 | } 400 | } 401 | 402 | .TagCategory-lastDiscussion.empty .LastDiscussion { 403 | font-weight: 550; 404 | } 405 | 406 | .LockedText { 407 | display: flex; 408 | align-items: center; 409 | color: @categories-color-highlighted; 410 | 411 | .LockedText-content, 412 | .LockedText-topRow, 413 | .LockedText-bottomRow { 414 | display: flex; 415 | } 416 | .LockedText-topRow { 417 | color: @categories-color-white; 418 | font-weight: 550; 419 | } 420 | } 421 | 422 | .LastDiscussion { 423 | display: flex; 424 | align-items: center; 425 | color: @categories-color-highlighted; 426 | 427 | .LastDiscussion-userCardContainer { 428 | display: block; 429 | position: relative; 430 | } 431 | 432 | .UserCard { 433 | position: absolute; 434 | top: -20px; 435 | left: -200px; 436 | z-index: 1030; 437 | -webkit-transition: opacity 0.2s, transform 0.2s; 438 | -o-transition: opacity 0.2s, transform 0.2s; 439 | transition: opacity 0.2s, transform 0.2s; 440 | transform: scale(0.95); 441 | transform-origin: left top; 442 | opacity: 1; 443 | } 444 | 445 | &:hover { 446 | text-decoration: none; 447 | 448 | .LastDiscussion-topRow { 449 | text-decoration: underline; 450 | } 451 | .LastDiscussion-bottomRow:hover ~ .LastDiscussion-topRow { 452 | text-decoration: none !important; 453 | } 454 | 455 | .LastDiscussion-avatar:hover 456 | ~ .LastDiscussion-content 457 | .LastDiscussion-topRow { 458 | text-decoration: none !important; 459 | } 460 | } 461 | 462 | .LastDiscussion-content, 463 | .LastDiscussion-topRow, 464 | .LastDiscussion-bottomRow { 465 | display: flex; 466 | } 467 | 468 | .LastDiscussion-topRow { 469 | color: @categories-color-white; 470 | font-weight: 550; 471 | } 472 | 473 | .LastDiscussion-bottomRow { 474 | color: @categories-color-white-muted; 475 | 476 | .LastDiscussion-usernameLink, 477 | .username, 478 | time { 479 | display: contents; 480 | } 481 | 482 | .LastDiscussion-usernameLink { 483 | color: @categories-color-white-muted; 484 | } 485 | } 486 | } 487 | 488 | .TagCategory.SubCategory { 489 | .TagCategory-lastDiscussion.empty { 490 | .LastDiscussion { 491 | color: @categories-sub-color-highlighted; 492 | } 493 | } 494 | .LastDiscussion { 495 | .LastDiscussion-topRow { 496 | color: @categories-sub-color-highlighted; 497 | } 498 | .LastDiscussion-bottomRow { 499 | color: @categories-sub-color-muted; 500 | .LastDiscussion-usernameLink { 501 | color: @categories-sub-color-muted; 502 | } 503 | } 504 | } 505 | } 506 | 507 | @media @phone { 508 | .TagCategory-content { 509 | flex-direction: column; 510 | } 511 | 512 | .TagCategory-alignEnd { 513 | flex-direction: column; 514 | margin-top: 25px; 515 | } 516 | 517 | .StatWidget { 518 | flex-direction: row; 519 | .StatWidget-count { 520 | margin-right: 4px; 521 | } 522 | } 523 | 524 | .TagCategory-lockedText { 525 | border-top: 1px solid rgba(0, 0, 0, 0.15); 526 | margin-top: 10px; 527 | padding: 7px 0; 528 | } 529 | 530 | .LockedText-content { 531 | flex-direction: row-reverse; 532 | } 533 | 534 | .TagCategory-lastDiscussion { 535 | border-top: 1px solid rgba(0, 0, 0, 0.15); 536 | margin-top: 10px; 537 | padding: 7px 0; 538 | } 539 | 540 | .LastDiscussion-avatar { 541 | display: none; 542 | } 543 | 544 | .LastDiscussion-content { 545 | flex-direction: row-reverse; 546 | } 547 | 548 | .LastDiscussion-topRow { 549 | margin-right: 5px; 550 | } 551 | 552 | .TagCategory-icon, 553 | .TagCategory-main, 554 | .TagCategory-stats { 555 | padding: 0 15px 0 0px; 556 | } 557 | 558 | .LastDiscussion .UserCard { 559 | display: none; 560 | } 561 | } 562 | 563 | @media @tablet-up { 564 | .TagCategory-content { 565 | flex-direction: row; 566 | } 567 | 568 | .TagCategory-alignEnd { 569 | flex-direction: row; 570 | } 571 | .TagCategory-alignStart { 572 | max-width: 50%; 573 | margin-bottom: 0; 574 | } 575 | 576 | .TagCategory.SubCategory { 577 | .TagCategory-alignStart { 578 | max-width: 40%; 579 | } 580 | } 581 | 582 | .StatWidget { 583 | flex-direction: column; 584 | .StatWidget-count { 585 | margin-right: 0; 586 | } 587 | } 588 | 589 | .TagCategory-lastDiscussion { 590 | border-top: none; 591 | margin-top: 0; 592 | padding: 0 15px; 593 | } 594 | 595 | .TagCategory-lockedText { 596 | border-top: none; 597 | margin-top: 0; 598 | padding: 0 15px; 599 | width: 280px; 600 | } 601 | 602 | .LastDiscussion-avatar { 603 | display: block; 604 | padding-right: 10px; 605 | } 606 | 607 | .LastDiscussion-content { 608 | flex-direction: column-reverse; 609 | } 610 | 611 | .TagCategory-icon, 612 | .TagCategory-main, 613 | .TagCategory-stats { 614 | padding: 0 15px; 615 | } 616 | 617 | .TagCategory-lastDiscussion.empty { 618 | justify-content: center; 619 | } 620 | 621 | .TagCategory-lastDiscussion { 622 | width: 280px; 623 | } 624 | 625 | .TagCategory.SubCategory .TagCategory-lastDiscussion { 626 | width: 250px; 627 | } 628 | } 629 | 630 | @media (min-width: 768px) { 631 | .TagCategory-content.compactMobile { 632 | flex-direction: column 633 | } 634 | .TagCategory.SubCategory.compactMobile { 635 | margin-right: 30px; 636 | } 637 | } 638 | @media @phone { 639 | .TagCategoryList > .TagCategory.compactMobile { 640 | margin-bottom: 15px; 641 | } 642 | 643 | .compactMobile { 644 | .TagCategory-description { 645 | display: none; 646 | } 647 | &.hover:hover { 648 | opacity: 90%; 649 | } 650 | } 651 | 652 | .ParentCategory.compactMobile { 653 | > .TagCategory-content { 654 | padding: 10px 15px; 655 | 656 | > :nth-child(1) { 657 | order: 1; 658 | } 659 | > :nth-child(2) { 660 | order: 3; 661 | } 662 | > :nth-child(3) { 663 | order: 2; 664 | } 665 | 666 | .TagCategory-alignStart { 667 | justify-content: space-between; 668 | } 669 | 670 | .TagCategory-toggleArrow { 671 | display: flex; 672 | font-size: 30px; 673 | margin-right: 20px; 674 | &.hover:hover { 675 | opacity: 50%; 676 | } 677 | } 678 | 679 | > .TagCategory-alignEnd { 680 | margin-top: 0; 681 | flex-direction: row-reverse; 682 | justify-content: space-around; 683 | align-content: center; 684 | padding-top: 5px; 685 | border-top: 1px solid black; 686 | 687 | .TagCategory-lastDiscussion { 688 | margin-top: 0; 689 | border-top: 0; 690 | 691 | .LastDiscussion-usernameLink { 692 | display: none; 693 | } 694 | } 695 | .TagCategory-lockedText { 696 | margin-top: 0; 697 | border-top: 0; 698 | } 699 | } 700 | .TagCategory-subTagList { 701 | > :last-child { 702 | margin-bottom: 10px; 703 | } 704 | } 705 | } 706 | } 707 | 708 | .SubCategory.compactMobile { 709 | .TagCategory-content { 710 | flex-direction: row; 711 | padding: 7px 15px; 712 | background-color: fade(@categories-sub-color-background, 65%); 713 | margin-right: 30px; 714 | } 715 | .TagCategory-name { 716 | font-size: 16px; 717 | } 718 | 719 | .TagCategory-toggleArrow { 720 | display: none !important; 721 | } 722 | 723 | .TagCategory-alignEnd { 724 | margin-top: 0; 725 | justify-content: center; 726 | border-top: 0; 727 | 728 | .TagCategory-lastDiscussion { 729 | display: none; 730 | } 731 | .TagCategory-lockedText { 732 | display: none; 733 | } 734 | } 735 | .StatWidgetList { 736 | display: flex; 737 | 738 | > .StatWidget { 739 | border-right: 2px solid @categories-color-white-more-muted; 740 | } 741 | } 742 | } 743 | } 744 | -------------------------------------------------------------------------------- /resources/locale/en.yml: -------------------------------------------------------------------------------- 1 | askvortsov-categories: 2 | admin: 3 | basics: 4 | categories_label: Categories 5 | labels: 6 | keep_tags_nav: Keep the tags page link in the nav sidebar? 7 | child_bare_icon: Bare child icons? 8 | compact_mobile_mode: Compact mobile mode 9 | full_page_desktop: Full page desktop? 10 | parent_remove_icon: Hide icons for top-level tags? 11 | parent_remove_description: Hide descriptions for top-level tags? 12 | parent_remove_stats: Hide stats for top-level tags? 13 | parent_remove_last_discussion: Hide most recent discussions for top-level tags? 14 | small_forum_optimized: Optimize for small forums? 15 | help: 16 | child_bare_icon: Should icons on child categories be displayed without a circular background? 17 | full_page_desktop: Should the sidebar nav menu be collapsed to a row (like on the traditional tags page)? This will also hide widgets (such as Friends of Flarum Forum Statistics) from the categories page navbar. 18 | small_forum_optimized: This will give more accurate discussion/post counts, but will slow medium and large forums dramatically. 19 | headings: 20 | nav: Navigation 21 | layout: Layout 22 | parent_display: Parent Category Display 23 | child_display: Child Category Display 24 | performance: Performance 25 | title: Categories Settings 26 | 27 | forum: 28 | all_categories: 29 | meta_description_text: All Categories 30 | meta_title_text: => askvortsov-categories.ref.categories 31 | header: 32 | back_to_categories_tooltip: Back to Categories 33 | index: 34 | categories_link: => askvortsov-categories.ref.categories 35 | stat-widgets: 36 | discussion_label: Discussions 37 | post_label: Posts 38 | last_discussion_widget: 39 | no_discussions: No Discussions (Yet!) 40 | 41 | ref: 42 | categories: Categories -------------------------------------------------------------------------------- /src/Content/Categories.php: -------------------------------------------------------------------------------- 1 | api = $api; 73 | $this->view = $view; 74 | $this->tags = $tags; 75 | $this->settings = $settings; 76 | $this->translator = $translator; 77 | $this->url = $url; 78 | } 79 | 80 | public function __invoke(Document $document, Request $request) 81 | { 82 | $apiDocument = $this->getTagsDocument($request); 83 | $tags = collect(Arr::get($apiDocument, 'data', [])); 84 | 85 | $childTags = $tags->where('attributes.isChild', true); 86 | $primaryTags = $tags->where('attributes.isChild', false)->where('attributes.position', '!==', null)->sortBy('attributes.position'); 87 | $secondaryTags = $tags->where('attributes.isChild', false)->where('attributes.position', '===', null)->sortBy('attributes.name'); 88 | 89 | $children = $primaryTags->mapWithKeys(function ($tag) use ($childTags) { 90 | $childIds = collect(Arr::get($tag, 'relationships.children.data'))->pluck('id'); 91 | 92 | return [$tag['id'] => $childTags->whereIn('id', $childIds)->sortBy('position')]; 93 | }); 94 | 95 | $defaultRoute = $this->settings->get('default_route'); 96 | $document->title = $this->translator->trans('askvortsov-categories.forum.all_categories.meta_title_text'); 97 | $document->meta['description'] = $this->translator->trans('askvortsov-categories.forum.all_categories.meta_description_text'); 98 | $document->content = $this->view->make('tags::frontend.content.tags', compact('primaryTags', 'secondaryTags', 'children')); 99 | $document->canonicalUrl = $defaultRoute === '/categories' ? $this->url->to('forum')->base() : $request->getUri()->withQuery(''); 100 | $document->payload['apiDocument'] = $apiDocument; 101 | 102 | return $document; 103 | } 104 | 105 | private function getTagsDocument(Request $request) 106 | { 107 | return json_decode($this->api->withParentRequest($request)->withQueryParams([ 108 | 'include' => 'children,parent,lastPostedDiscussion,lastPostedDiscussion.lastPostedUser', 109 | ])->get('/tags')->getBody(), true); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | discussion->tags as $tag) { 27 | // We do not count private discussions in tags 28 | if (!$post->is_private && !$post->discussion->is_private) { 29 | $tag->post_count += $delta; 30 | } 31 | 32 | $tag->save(); 33 | } 34 | } 35 | } 36 | --------------------------------------------------------------------------------