├── .npmignore ├── static ├── style.scss ├── lib │ ├── admin.js │ ├── external.js │ └── main.js ├── templates │ ├── admin │ │ └── plugins │ │ │ ├── nodebb-plugin-recent-cards │ │ │ ├── tests │ │ │ │ └── external.tpl │ │ │ └── widget.tpl │ │ │ └── recentcards.tpl │ └── partials │ │ └── nodebb-plugin-recent-cards │ │ ├── header.tpl │ │ └── external │ │ └── style.tpl ├── slick │ ├── slick-theme.css │ ├── slick.css │ └── slick.min.js └── external │ └── bootstrap-grid.css ├── eslint.config.mjs ├── .gitattributes ├── package.json ├── plugin.json ├── LICENSE ├── README.md ├── .gitignore └── library.js /.npmignore: -------------------------------------------------------------------------------- 1 | sftp-config.json 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /static/style.scss: -------------------------------------------------------------------------------- 1 | .recent-cards-plugin { 2 | .topic-info { 3 | height: 9em; 4 | > p { 5 | margin-bottom: 1.5em; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import serverConfig from 'eslint-config-nodebb'; 4 | import publicConfig from 'eslint-config-nodebb/public'; 5 | 6 | export default [ 7 | ...publicConfig, 8 | ...serverConfig, 9 | { 10 | ignores: ['static/slick/**'], 11 | }, 12 | ]; 13 | -------------------------------------------------------------------------------- /static/lib/admin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define('admin/plugins/recentcards', ['settings'], function (settings) { 4 | const admin = {}; 5 | admin.init = function () { 6 | settings.sync('recentcards', $('#recentcards')); 7 | 8 | $('#save').click(function () { 9 | settings.persist('recentcards', $('#recentcards')); 10 | }); 11 | }; 12 | return admin; 13 | }); 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /static/lib/external.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(document).ready(function () { 4 | $.get(window.path_to_nodebb + '/plugins/nodebb-plugin-recent-cards/render', {}, function (html) { 5 | html = $(html); 6 | if (html.length !== 5) { 7 | return; 8 | } 9 | 10 | $('#nodebb-plugin-recent-cards').html($(html[2])); 11 | 12 | var ajaxifyData = $(html[4]); 13 | 14 | if (ajaxifyData.length) { 15 | ajaxifyData = JSON.parse(ajaxifyData.text()); 16 | 17 | if (ajaxifyData.recentCards.enableCarousel) { 18 | $('#nodebb-plugin-recent-cards .recent-cards').bxSlider({ 19 | slideWidth: 292, 20 | minSlides: 1, 21 | maxSlides: 4, 22 | pager: ajaxifyData.recentCards.enableCarouselPagination ? true : false, 23 | }); 24 | } else { 25 | $('#nodebb-plugin-recent-cards .recent-cards').removeClass('carousel-mode'); 26 | } 27 | 28 | if ($.timeago) { 29 | $('#nodebb-plugin-recent-cards .timeago').not('[datetime]').timeago(); 30 | } 31 | } 32 | }); 33 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodebb-plugin-recent-cards", 3 | "version": "3.3.4", 4 | "description": "Add lavender-style cards of recent topics to Persona's category homepage", 5 | "main": "library.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/psychobunny/nodebb-plugin-recent-cards" 9 | }, 10 | "scripts": { 11 | "lint": "eslint ." 12 | }, 13 | "keywords": [ 14 | "nodebb", 15 | "plugin", 16 | "recent", 17 | "cards", 18 | "persona" 19 | ], 20 | "author": { 21 | "name": "psychobunny", 22 | "email": "psycho.bunny@hotmail.com" 23 | }, 24 | "license": "BSD-2-Clause", 25 | "bugs": { 26 | "url": "https://github.com/psychobunny/nodebb-plugin-recent-cards/issues" 27 | }, 28 | "readmeFilename": "README.md", 29 | "devDependencies": { 30 | "eslint": "^9.x", 31 | "eslint-config-nodebb": "1.1.11", 32 | "eslint-plugin-import": "2.32.0" 33 | }, 34 | "nbbpm": { 35 | "compatibility": "^4.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /static/templates/admin/plugins/nodebb-plugin-recent-cards/tests/external.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "nodebb-plugin-recent-cards", 3 | "name": "Recent Cards plugin for NodeBB's Persona Theme", 4 | "description": "Add lavender-style cards of recent topics to Persona's category homepage", 5 | "url": "https://github.com/NodeBB/nodebb-plugin-recent-cards", 6 | "library": "./library.js", 7 | "hooks": [ 8 | { 9 | "hook": "static:app.load", "method": "init" 10 | }, 11 | { 12 | "hook": "filter:config.get", "method": "getConfig" 13 | }, 14 | { 15 | "hook": "filter:admin.header.build", "method": "addAdminNavigation" 16 | }, 17 | { 18 | "hook": "filter:widgets.getWidgets", "method": "defineWidgets" 19 | }, 20 | { 21 | "hook": "filter:widget.render:recentCards", "method": "renderWidget" 22 | } 23 | ], 24 | "staticDirs": { 25 | "static": "./static" 26 | }, 27 | "css": [ 28 | "static/slick/slick.css", 29 | "static/slick/slick-theme.css" 30 | ], 31 | "scss": [ 32 | "static/style.scss" 33 | ], 34 | "scripts": [ 35 | "static/slick/slick.min.js", 36 | "static/lib/main.js" 37 | ], 38 | "modules": { 39 | "../admin/plugins/recentcards.js": "static/lib/admin.js" 40 | }, 41 | "templates": "static/templates" 42 | } 43 | -------------------------------------------------------------------------------- /static/templates/admin/plugins/recentcards.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 |
7 |
8 |
9 | 10 | 13 |
14 | 15 |
16 | 17 | 20 |
21 | 22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014, psychobunny 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /static/templates/admin/plugins/nodebb-plugin-recent-cards/widget.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Set the category IDs you want to display this widget on (separated by commas)

5 |
6 | 7 |
8 | 9 | 10 |

Set the category IDs you want to pull topics from (separated by commas)

11 |
12 | 13 |
14 | 15 | 16 |

Set the topic IDs you want to display in the widget (separated by commas). This overrides category IDs setting.

17 |
18 | 19 |
20 | 21 | 26 |
27 | 28 |
29 | 30 | 34 |
35 | 36 |
37 | 38 | 44 |
45 | -------------------------------------------------------------------------------- /static/lib/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(document).ready(function () { 4 | $(window).on('action:ajaxify.end', async function () { 5 | const recentCards = $('.recent-cards'); 6 | if (!recentCards.length) { 7 | return; 8 | } 9 | const translator = await app.require('translator'); 10 | const [nextTitle, prevTitle] = await translator.translateKeys([ 11 | '[[global:pagination.nextpage]]', '[[global:pagination.previouspage]]', 12 | ], config.userLang); 13 | if (!config.recentCards || !config.recentCards.enableCarousel) { 14 | recentCards.removeClass('carousel-mode'); 15 | return; 16 | } 17 | const rtl = $('html').attr('data-dir') === 'rtl'; 18 | const arrowClasses = 'position-absolute top-50 translate-middle-y p-1 z-1'; 19 | const nextArrow = ` 20 | 21 | `; 22 | const prevArrow = ` 23 | 24 | `; 25 | const slideCount = parseInt(config.recentCards.maxSlides, 10) || 4; 26 | const slideMargin = 16; 27 | const env = utils.findBootstrapEnvironment(); 28 | if (['xxl', 'xl', 'lg'].includes(env)) { 29 | $('.recent-card').css({ 30 | width: (recentCards.width() - ((slideCount - 1) * slideMargin)) / slideCount, 31 | }); 32 | } 33 | recentCards.slick({ 34 | infinite: false, 35 | slidesToShow: slideCount, 36 | slidesToScroll: slideCount, 37 | rtl: rtl, 38 | variableWidth: true, 39 | dots: !!config.recentCards.enableCarouselPagination, 40 | nextArrow: nextArrow, 41 | prevArrow: prevArrow, 42 | responsive: [{ 43 | breakpoint: 992, // md 44 | settings: { 45 | slidesToShow: 3, 46 | slidesToScroll: 2, 47 | infinite: false, 48 | }, 49 | }, { 50 | breakpoint: 768, // sm/xs 51 | settings: { 52 | slidesToShow: 2, 53 | slidesToScroll: 1, 54 | infinite: false, 55 | }, 56 | }], 57 | }); 58 | recentCards.removeClass('overflow-hidden'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /static/slick/slick-theme.css: -------------------------------------------------------------------------------- 1 | 2 | /* modified from https://gist.github.com/vivekkumawat/c8351e28412a194aac977bf996c4a6d8 */ 3 | 4 | /* CSS For Slick Slider */ 5 | /* Note: Don't use slick-theme.css file */ 6 | 7 | /* Adding margin between slides */ 8 | .slick-slide { 9 | margin: 0 8px; 10 | } 11 | .slick-list { 12 | margin: 0; 13 | } 14 | 15 | /* Arrows */ 16 | .slick-prev.slick-disabled, 17 | .slick-next.slick-disabled 18 | { 19 | opacity: .25; 20 | } 21 | .slick-prev 22 | { 23 | left: 0px; 24 | } 25 | [data-dir='rtl'] .slick-prev 26 | { 27 | right: 0px; 28 | left: auto; 29 | } 30 | 31 | .slick-next 32 | { 33 | right: 0px; 34 | } 35 | [data-dir='rtl'] .slick-next 36 | { 37 | right: auto; 38 | left: 0px; 39 | } 40 | 41 | /* Dots */ 42 | .slick-dotted.slick-slider 43 | { 44 | margin-bottom: 30px; 45 | } 46 | 47 | .slick-dots 48 | { 49 | position: absolute; 50 | bottom: -25px; 51 | 52 | display: block; 53 | 54 | width: 100%; 55 | padding: 0; 56 | margin: 0; 57 | 58 | list-style: none; 59 | 60 | text-align: center; 61 | } 62 | .slick-dots li 63 | { 64 | position: relative; 65 | 66 | display: inline-block; 67 | 68 | width: 20px; 69 | height: 20px; 70 | margin: 0 5px; 71 | padding: 0; 72 | 73 | cursor: pointer; 74 | } 75 | .slick-dots li button 76 | { 77 | font-size: 0; 78 | line-height: 0; 79 | 80 | display: block; 81 | 82 | width: 20px; 83 | height: 20px; 84 | padding: 5px; 85 | 86 | cursor: pointer; 87 | 88 | color: transparent; 89 | border: 0; 90 | outline: none; 91 | background: transparent; 92 | } 93 | .slick-dots li button:hover, 94 | .slick-dots li button:focus 95 | { 96 | outline: none; 97 | } 98 | .slick-dots li button:hover:before, 99 | .slick-dots li button:focus:before 100 | { 101 | opacity: 1; 102 | } 103 | .slick-dots li button:before 104 | { 105 | font-size: 8px; 106 | line-height: 20px; 107 | 108 | position: absolute; 109 | top: 0; 110 | left: 0; 111 | 112 | width: 20px; 113 | height: 20px; 114 | 115 | content: '•'; 116 | text-align: center; 117 | 118 | opacity: .25; 119 | color: black; 120 | 121 | -webkit-font-smoothing: antialiased; 122 | -moz-osx-font-smoothing: grayscale; 123 | } 124 | .slick-dots li.slick-active button:before 125 | { 126 | opacity: .75; 127 | color: black; 128 | } 129 | -------------------------------------------------------------------------------- /static/slick/slick.css: -------------------------------------------------------------------------------- 1 | /* Slider */ 2 | .slick-slider 3 | { 4 | position: relative; 5 | 6 | display: block; 7 | box-sizing: border-box; 8 | 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | 14 | -webkit-touch-callout: none; 15 | -khtml-user-select: none; 16 | -ms-touch-action: pan-y; 17 | touch-action: pan-y; 18 | -webkit-tap-highlight-color: transparent; 19 | } 20 | 21 | .slick-list 22 | { 23 | position: relative; 24 | 25 | display: block; 26 | overflow: hidden; 27 | 28 | margin: 0; 29 | padding: 0; 30 | } 31 | .slick-list:focus 32 | { 33 | outline: none; 34 | } 35 | .slick-list.dragging 36 | { 37 | cursor: pointer; 38 | cursor: hand; 39 | } 40 | 41 | .slick-slider .slick-track, 42 | .slick-slider .slick-list 43 | { 44 | -webkit-transform: translate3d(0, 0, 0); 45 | -moz-transform: translate3d(0, 0, 0); 46 | -ms-transform: translate3d(0, 0, 0); 47 | -o-transform: translate3d(0, 0, 0); 48 | transform: translate3d(0, 0, 0); 49 | } 50 | 51 | .slick-track 52 | { 53 | position: relative; 54 | top: 0; 55 | left: 0; 56 | 57 | display: block; 58 | margin-left: auto; 59 | margin-right: auto; 60 | } 61 | .slick-track:before, 62 | .slick-track:after 63 | { 64 | display: table; 65 | 66 | content: ''; 67 | } 68 | .slick-track:after 69 | { 70 | clear: both; 71 | } 72 | .slick-loading .slick-track 73 | { 74 | visibility: hidden; 75 | } 76 | 77 | .slick-slide 78 | { 79 | display: none; 80 | float: left; 81 | 82 | height: 100%; 83 | min-height: 1px; 84 | } 85 | [dir='rtl'] .slick-slide 86 | { 87 | float: right; 88 | } 89 | .slick-slide img 90 | { 91 | display: block; 92 | } 93 | .slick-slide.slick-loading img 94 | { 95 | display: none; 96 | } 97 | .slick-slide .topic-title img { 98 | display: inline-block 99 | } 100 | .slick-slide.dragging img 101 | { 102 | pointer-events: none; 103 | } 104 | .slick-initialized .slick-slide 105 | { 106 | display: block; 107 | } 108 | .slick-loading .slick-slide 109 | { 110 | visibility: hidden; 111 | } 112 | .slick-vertical .slick-slide 113 | { 114 | display: block; 115 | 116 | height: auto; 117 | 118 | border: 1px solid transparent; 119 | } 120 | .slick-arrow.slick-hidden { 121 | display: none; 122 | } -------------------------------------------------------------------------------- /static/templates/partials/nodebb-plugin-recent-cards/header.tpl: -------------------------------------------------------------------------------- 1 | {{{ if topics.length }}} 2 |
3 | {{{ if title }}} 4 |
{title}
5 | {{{ end }}} 6 | 7 | 54 |
55 | {{{end}}} 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recent Cards plugin for NodeBB's Persona Theme 2 | 3 | This is a plugin that creates a new widget that can be placed on various widget areas. It's inspired by the previous default theme, Lavender, which used Modern UI styling for the category layout. 4 | 5 | 6 | ## Installation 7 | 8 | Install via one-click activation in the Admin Control Panel or run the following command: 9 | 10 | npm i nodebb-plugin-recent-cards 11 | 12 | Then head over to Admin -> Extend -> Widgets and place the widget. Additional settings can be found at Admin -> Plugins -> Recent Cards and under the individual widget settings. 13 | 14 | ## Screenshot 15 | 16 | ![](https://i.imgur.com/r3NKmY1.jpg) 17 | 18 | # Standalone installation for external websites (Advanced) 19 | 20 | Use this plugin on any external non-nodebb site (ex. Wordpress, etc) to show recent topics from your NodeBB install. 21 | 22 | ### Header Scripts + Styles 23 | 24 | Place these in the `header` section of your external site, and replace all instances of `{forumURL}` to your forum's URL: 25 | 26 | ``` 27 | 28 | 31 | 32 | 33 | 34 | ``` 35 | 36 | If your external site doesn't have jQuery included, you will have include it above the previous lines. Get the latest jQuery at https://code.jquery.com/ 37 | 38 | You should also (optionally) require the jQuery Timeago library in order to display human-readable timestamps: 39 | 40 | ``` 41 | 42 | ``` 43 | 44 | If your external site doesn't have Bootstrap included, you will have to include this line as well in your `header`, which is the bare minimum (grid + responsive utilities) required for this plugin: 45 | 46 | ``` 47 | 48 | ``` 49 | 50 | Similarly, if your external site does not use FontAwesome, then you will have to include this line as well in order to display category icons: 51 | 52 | ``` 53 | 54 | ``` 55 | 56 | 57 | Finally, if you need to include the default Persona font to match your external site's recent cards with your forum's, then include this: 58 | 59 | ``` 60 | 61 | ``` 62 | 63 | 64 | ### Body Content 65 | 66 | Place the following code wherever you'd like recent cards to be displayed: 67 | 68 | ``` 69 |
70 | ``` 71 | 72 | ### Configure ACAO in NodeBB 73 | 74 | Under Settings -> Advanced in the NodeBB control panel, add the external site's URL to `Access-Control-Allow-Origin` 75 | 76 | ### Stuck? 77 | 78 | No problem! Visit https://yourforum.com/admin/plugins/nodebb-plugin-recent-cards/tests/external, which will render the standalone version of the plugin tailored for your website. Keep in mind that this includes all extra scripts and styling that you may not necessarily need if you already have Bootstrap, jQuery, etc. on your external site. 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | 217 | sftp-config.json 218 | node_modules/ 219 | 220 | *.sublime* -------------------------------------------------------------------------------- /library.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 3 | 'use strict'; 4 | 5 | const nconf = require.main.require('nconf'); 6 | const _ = require.main.require('lodash'); 7 | const validator = require.main.require('validator'); 8 | const db = require.main.require('./src/database'); 9 | const topics = require.main.require('./src/topics'); 10 | const settings = require.main.require('./src/settings'); 11 | const groups = require.main.require('./src/groups'); 12 | const user = require.main.require('./src/user'); 13 | 14 | const defaultSettings = { 15 | enableCarousel: 0, 16 | enableCarouselPagination: 0, 17 | minSlides: 1, 18 | maxSlides: 4, 19 | }; 20 | 21 | const plugin = module.exports; 22 | let app; 23 | 24 | plugin.init = async function (params) { 25 | app = params.app; 26 | const { router } = params; 27 | const routeHelpers = require.main.require('./src/routes/helpers'); 28 | routeHelpers.setupAdminPageRoute(router, '/admin/plugins/recentcards', renderAdmin); 29 | 30 | router.get('/plugins/nodebb-plugin-recent-cards/render', renderExternal); 31 | router.get('/plugins/nodebb-plugin-recent-cards/render/style.css', renderExternalStyle); 32 | router.get('/admin/plugins/nodebb-plugin-recent-cards/tests/external', testRenderExternal); 33 | 34 | plugin.settings = new settings('recentcards', '1.0.0', defaultSettings); 35 | }; 36 | 37 | plugin.addAdminNavigation = async function (header) { 38 | header.plugins.push({ 39 | route: '/plugins/recentcards', 40 | icon: 'fa-tint', 41 | name: 'Recent Cards', 42 | }); 43 | return header; 44 | }; 45 | 46 | plugin.defineWidgets = async function (widgets) { 47 | const groupNames = await db.getSortedSetRevRange('groups:visible:createtime', 0, -1); 48 | let groupsData = await groups.getGroupsData(groupNames); 49 | groupsData = groupsData.filter(Boolean); 50 | groupsData.forEach((group) => { 51 | group.name = validator.escape(String(group.name)); 52 | }); 53 | 54 | const html = await app.renderAsync('admin/plugins/nodebb-plugin-recent-cards/widget', { 55 | groups: groupsData, 56 | }); 57 | 58 | widgets.push({ 59 | widget: 'recentCards', 60 | name: 'Recent Cards', 61 | description: 'Recent topics carousel', 62 | content: html, 63 | }); 64 | return widgets; 65 | }; 66 | 67 | plugin.getConfig = async function (config) { 68 | config.recentCards = { 69 | title: plugin.settings.get('title'), 70 | opacity: plugin.settings.get('opacity'), 71 | textShadow: plugin.settings.get('shadow'), 72 | enableCarousel: plugin.settings.get('enableCarousel'), 73 | enableCarouselPagination: plugin.settings.get('enableCarouselPagination'), 74 | minSlides: plugin.settings.get('minSlides'), 75 | maxSlides: plugin.settings.get('maxSlides'), 76 | }; 77 | return config; 78 | }; 79 | 80 | plugin.renderWidget = async function (widget) { 81 | if (!isVisibleInCategory(widget)) { 82 | return null; 83 | } 84 | const topics = await getTopics(widget); 85 | 86 | const sort = widget.data.sort || 'recent'; 87 | const sorts = { 88 | create: sort === 'create', 89 | recent: sort === 'recent', 90 | posts: sort === 'posts', 91 | votes: sort === 'votes', 92 | }; 93 | 94 | widget.html = await app.renderAsync('partials/nodebb-plugin-recent-cards/header', { 95 | topics: topics, 96 | config: widget.templateData.config, 97 | title: widget.data.title || '', 98 | sorts: sorts, 99 | carouselMode: plugin.settings.get('enableCarousel'), 100 | }); 101 | return widget; 102 | }; 103 | 104 | function getIdsArray(data, field) { 105 | const ids = String(data[field] || ''); 106 | return ids.split(',').map(c => parseInt(c.trim(), 10)).filter(Boolean); 107 | } 108 | 109 | function isVisibleInCategory(widget) { 110 | const cids = getIdsArray(widget.data, 'cid'); 111 | return !( 112 | cids.length && 113 | (widget.templateData.template.category || widget.templateData.template.topic) && 114 | !cids.includes(parseInt(widget.templateData.cid, 10)) 115 | ); 116 | } 117 | 118 | async function getTopics(widget) { 119 | async function getTopicsFromSet(set) { 120 | let start = 0; 121 | const topicsData = []; 122 | 123 | do { 124 | let tids = await db.getSortedSetRevRangeByScore(set, start, 20, Date.now(), '-inf'); 125 | if (!tids.length) { 126 | break; 127 | } 128 | 129 | tids = await topics.filterNotIgnoredTids(tids, widget.uid); 130 | let nextTopics = await topics.getTopics(tids, { 131 | uid: widget.uid, 132 | teaserPost: widget.data.teaserPost || 'first', 133 | }); 134 | 135 | nextTopics = await user.blocks.filter(widget.uid, nextTopics); 136 | topicsData.push(...nextTopics); 137 | start += 20; 138 | } while (topicsData.length < 20); 139 | return { topics: topicsData.slice(0, 20) }; 140 | } 141 | 142 | let topicsData = { 143 | topics: [], 144 | }; 145 | let filterCids = getIdsArray(widget.data, 'topicsFromCid'); 146 | if (!filterCids.length && widget.templateData.cid) { 147 | filterCids = [parseInt(widget.templateData.cid, 10)]; 148 | } 149 | 150 | widget.data.sort = widget.data.sort || 'recent'; 151 | let fromGroups = widget.data.fromGroups || []; 152 | if (fromGroups && !Array.isArray(fromGroups)) { 153 | fromGroups = [fromGroups]; 154 | } 155 | // hard coded to show these topic tids only 156 | const topicsTids = getIdsArray(widget.data, 'topicsTids'); 157 | if (topicsTids.length) { 158 | topicsData.topics = await topics.getTopics(topicsTids, { 159 | uid: widget.uid, 160 | teaserPost: widget.data.teaserPost || 'first', 161 | }); 162 | } else if (fromGroups.length) { 163 | const uids = _.uniq(_.flatten(await groups.getMembersOfGroups(fromGroups))); 164 | const sets = uids.map((uid) => { 165 | if (filterCids.length) { 166 | return filterCids.map(cid => `cid:${cid}:uid:${uid}:tids`); 167 | } 168 | return `uid:${uid}:topics`; 169 | }); 170 | topicsData = await getTopicsFromSet(sets.flat()); 171 | topicsData.topics.sort((t1, t2) => { 172 | if (widget.data.sort === 'recent') { 173 | return t2.lastposttime - t1.lastposttime; 174 | } else if (widget.data.sort === 'votes') { 175 | return t2.votes - t1.votes; 176 | } else if (widget.data.sort === 'posts') { 177 | return t2.postcount - t1.postcount; 178 | } 179 | return 0; 180 | }); 181 | } else if (filterCids.length) { 182 | let searchSuffix = ''; 183 | if (widget.data.sort === 'recent') { 184 | searchSuffix += ':lastposttime'; 185 | } else if (widget.data.sort === 'votes' || widget.data.sort === 'posts' || widget.data.sort === 'create') { 186 | searchSuffix += `:${widget.data.sort}`; 187 | } 188 | topicsData = await getTopicsFromSet( 189 | filterCids.map(cid => `cid:${cid}:tids${searchSuffix}`) 190 | ); 191 | } else { 192 | const map = { 193 | votes: 'topics:votes', 194 | posts: 'topics:posts', 195 | recent: 'topics:recent', 196 | create: 'topics:tid', 197 | }; 198 | topicsData = await getTopicsFromSet(map[widget.data.sort]); 199 | } 200 | 201 | let i = 0; 202 | const cids = []; 203 | let finalTopics = []; 204 | 205 | if (!plugin.settings.get('enableCarousel')) { 206 | while (finalTopics.length < 4 && i < topicsData.topics.length) { 207 | const cid = parseInt(topicsData.topics[i].cid, 10); 208 | 209 | if (filterCids.length || !cids.includes(cid) || topicsData.topics.length <= 4) { 210 | cids.push(cid); 211 | finalTopics.push(topicsData.topics[i]); 212 | } 213 | 214 | i += 1; 215 | } 216 | } else { 217 | finalTopics = topicsData.topics; 218 | } 219 | return finalTopics; 220 | } 221 | 222 | async function renderExternal(req, res, next) { 223 | try { 224 | const topics = await getTopics({ 225 | uid: req.uid, 226 | data: { 227 | teaserPost: 'first', 228 | }, 229 | templateData: {}, 230 | }); 231 | 232 | res.render('partials/nodebb-plugin-recent-cards/header', { 233 | topics: topics, 234 | config: { 235 | relative_path: nconf.get('url'), 236 | }, 237 | }); 238 | } catch (err) { 239 | next(err); 240 | } 241 | } 242 | 243 | function renderExternalStyle(req, res) { 244 | res.render('partials/nodebb-plugin-recent-cards/external/style', { 245 | forumURL: nconf.get('url'), 246 | }); 247 | } 248 | 249 | function testRenderExternal(req, res) { 250 | res.render('admin/plugins/nodebb-plugin-recent-cards/tests/external', { 251 | forumURL: nconf.get('url'), 252 | }); 253 | } 254 | 255 | async function renderAdmin(req, res) { 256 | res.render('admin/plugins/recentcards', { 257 | title: 'Recent Cards', 258 | }); 259 | } 260 | -------------------------------------------------------------------------------- /static/templates/partials/nodebb-plugin-recent-cards/external/style.tpl: -------------------------------------------------------------------------------- 1 | #nodebb-plugin-recent-cards .topic-info, #nodebb-plugin-recent-cards .description, #nodebb-plugin-recent-cards .post-count { 2 | font-family: Roboto; 3 | } 4 | 5 | #nodebb-plugin-recent-cards .topic-info .h4 { 6 | margin-top: 10px; 7 | margin-bottom: 10px; 8 | font-weight: 500; 9 | line-height: 1.1; 10 | font-size: 18px; 11 | } 12 | 13 | #nodebb-plugin-recent-cards .bx-viewport { 14 | height: 150px !important; 15 | } 16 | 17 | #nodebb-plugin-recent-cards .categories { 18 | display: none; 19 | } 20 | 21 | #nodebb-plugin-recent-cards .recent-cards { 22 | list-style-type: none; 23 | padding: 0; 24 | } 25 | 26 | #nodebb-plugin-recent-cards .recent-cards .recent-card { 27 | cursor: pointer; 28 | height: 110px; 29 | width: 100%; 30 | padding: 10px; 31 | position: relative; 32 | margin-bottom: 10px; 33 | } 34 | 35 | #nodebb-plugin-recent-cards .recent-cards .recent-card .bg { 36 | position: absolute; 37 | top: 0; 38 | left: 0; 39 | width: 100%; 40 | height: 100%; 41 | background-position: 50% 50%; 42 | background-size: cover; 43 | } 44 | 45 | #nodebb-plugin-recent-cards .recent-cards .recent-card .bg:hover { 46 | filter: brightness(115%); 47 | -webkit-filter: brightness(115%); 48 | } 49 | 50 | #nodebb-plugin-recent-cards .recent-cards .recent-card .topic-info { 51 | position: absolute; 52 | top: 10px; 53 | left: 10px; 54 | pointer-events: none; 55 | padding-right: 15px; 56 | width: 100%; 57 | text-overflow: ellipsis; 58 | white-space: nowrap; 59 | overflow: hidden; 60 | } 61 | 62 | #nodebb-plugin-recent-cards .recent-cards .recent-card .topic-info .description { 63 | width: 100%; 64 | text-overflow: ellipsis; 65 | white-space: nowrap; 66 | overflow: hidden; 67 | font-size: 11px; 68 | } 69 | 70 | #nodebb-plugin-recent-cards .recent-cards .recent-card .post-count { 71 | position: absolute; 72 | bottom: 5px; 73 | right: 10px; 74 | pointer-events: none; 75 | } 76 | 77 | #nodebb-plugin-recent-cards .recent-cards .recent-card .icon { 78 | position: absolute; 79 | bottom: 5px; 80 | left: 10px; 81 | pointer-events: none; 82 | width: 80%; 83 | text-overflow: ellipsis; 84 | white-space: nowrap; 85 | overflow: visible; 86 | font-size: 20px; 87 | } 88 | 89 | #nodebb-plugin-recent-cards .recent-cards.carousel-mode { 90 | max-height: 110px; 91 | overflow: hidden; 92 | } 93 | 94 | 95 | /** 96 | * BxSlider v4.1.2 - Fully loaded, responsive content slider 97 | * http://bxslider.com 98 | * 99 | * Written by: Steven Wanderski, 2014 100 | * http://stevenwanderski.com 101 | * (while drinking Belgian ales and listening to jazz) 102 | * 103 | * CEO and founder of bxCreative, LTD 104 | * http://bxcreative.com 105 | */ 106 | 107 | 108 | /** RESET AND LAYOUT 109 | ===================================*/ 110 | 111 | #nodebb-plugin-recent-cards .bx-wrapper { 112 | position: relative; 113 | margin: 0 auto 60px; 114 | padding: 0; 115 | *zoom: 1; 116 | } 117 | 118 | #nodebb-plugin-recent-cards .bx-wrapper img { 119 | max-width: 100%; 120 | display: block; 121 | } 122 | 123 | /** THEME 124 | ===================================*/ 125 | 126 | #nodebb-plugin-recent-cards .bx-wrapper .bx-viewport { 127 | border: 5px solid transparent; 128 | left: -5px; 129 | background: none; 130 | 131 | /*fix other elements on the page moving (on Chrome)*/ 132 | -webkit-transform: translatez(0); 133 | -moz-transform: translatez(0); 134 | -ms-transform: translatez(0); 135 | -o-transform: translatez(0); 136 | transform: translatez(0); 137 | } 138 | 139 | #nodebb-plugin-recent-cards .bx-wrapper .bx-pager, 140 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-auto { 141 | position: absolute; 142 | bottom: -15px; 143 | width: 100%; 144 | } 145 | 146 | /* LOADER */ 147 | 148 | #nodebb-plugin-recent-cards .bx-wrapper .bx-loading { 149 | min-height: 50px; 150 | background: url({forumURL}/plugins/nodebb-plugin-recent-cards/static/bxslider/images/bx_loader.gif) center center no-repeat #fff; 151 | height: 100%; 152 | width: 100%; 153 | position: absolute; 154 | top: 0; 155 | left: 0; 156 | z-index: 1; 157 | } 158 | 159 | /* PAGER */ 160 | 161 | #nodebb-plugin-recent-cards .bx-wrapper .bx-pager { 162 | text-align: center; 163 | font-size: .85em; 164 | font-family: Arial; 165 | font-weight: bold; 166 | color: #666; 167 | padding-top: 20px; 168 | } 169 | 170 | #nodebb-plugin-recent-cards .bx-wrapper .bx-pager .bx-pager-item, 171 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-auto .bx-controls-auto-item { 172 | display: inline-block; 173 | *zoom: 1; 174 | *display: inline; 175 | } 176 | 177 | #nodebb-plugin-recent-cards .bx-wrapper .bx-pager.bx-default-pager a { 178 | background: #666; 179 | text-indent: -9999px; 180 | display: block; 181 | width: 10px; 182 | height: 10px; 183 | margin: 0 5px; 184 | outline: 0; 185 | -moz-border-radius: 5px; 186 | -webkit-border-radius: 5px; 187 | border-radius: 5px; 188 | } 189 | 190 | #nodebb-plugin-recent-cards .bx-wrapper .bx-pager.bx-default-pager a:hover, 191 | #nodebb-plugin-recent-cards .bx-wrapper .bx-pager.bx-default-pager a.active { 192 | background: #000; 193 | } 194 | 195 | /* DIRECTION CONTROLS (NEXT / PREV) */ 196 | 197 | #nodebb-plugin-recent-cards .bx-wrapper .bx-prev { 198 | left: -12px; 199 | cursor: pointer; 200 | background: url({forumURL}/plugins/nodebb-plugin-recent-cards/static/bxslider/images/controls.png) no-repeat 0 -32px; 201 | } 202 | 203 | #nodebb-plugin-recent-cards .bx-wrapper .bx-next { 204 | right: -10px; 205 | cursor: pointer; 206 | background: url({forumURL}/plugins/nodebb-plugin-recent-cards/static/bxslider/images/controls.png) no-repeat -43px -32px; 207 | } 208 | 209 | #nodebb-plugin-recent-cards .bx-wrapper .bx-prev:hover { 210 | background-position: 0 0; 211 | } 212 | 213 | #nodebb-plugin-recent-cards .bx-wrapper .bx-next:hover { 214 | background-position: -43px 0; 215 | } 216 | 217 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-direction a { 218 | position: absolute; 219 | top: 50%; 220 | margin-top: -16px; 221 | outline: 0; 222 | width: 32px; 223 | height: 32px; 224 | text-indent: -9999px; 225 | z-index: 2; 226 | opacity: 0.5; 227 | -webkit-transition: opacity 0.25s ease-out; 228 | -moz-transition: opacity 0.25s ease-out; 229 | -ms-transition: opacity 0.25s ease-out; 230 | -o-transition: opacity 0.25s ease-out; 231 | transition: opacity 0.25s ease-out; 232 | } 233 | 234 | #nodebb-plugin-recent-cards .bx-wrapper:hover .bx-controls-direction a { 235 | opacity: 1; 236 | } 237 | 238 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-direction a.disabled { 239 | display: none; 240 | } 241 | 242 | /* AUTO CONTROLS (START / STOP) */ 243 | 244 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-auto { 245 | text-align: center; 246 | } 247 | 248 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-auto .bx-start { 249 | display: block; 250 | text-indent: -9999px; 251 | width: 10px; 252 | height: 11px; 253 | outline: 0; 254 | background: url({forumURL}/plugins/nodebb-plugin-recent-cards/static/bxslider/images/controls.png) -86px -11px no-repeat; 255 | margin: 0 3px; 256 | } 257 | 258 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-auto .bx-start:hover, 259 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-auto .bx-start.active { 260 | background-position: -86px 0; 261 | } 262 | 263 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-auto .bx-stop { 264 | display: block; 265 | text-indent: -9999px; 266 | width: 9px; 267 | height: 11px; 268 | outline: 0; 269 | background: url({forumURL}/plugins/nodebb-plugin-recent-cards/static/bxslider/images/controls.png) -86px -44px no-repeat; 270 | margin: 0 3px; 271 | } 272 | 273 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-auto .bx-stop:hover, 274 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls-auto .bx-stop.active { 275 | background-position: -86px -33px; 276 | } 277 | 278 | /* PAGER WITH AUTO-CONTROLS HYBRID LAYOUT */ 279 | 280 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls.bx-has-controls-auto.bx-has-pager .bx-pager { 281 | text-align: left; 282 | width: 80%; 283 | } 284 | 285 | #nodebb-plugin-recent-cards .bx-wrapper .bx-controls.bx-has-controls-auto.bx-has-pager .bx-controls-auto { 286 | right: 0; 287 | width: 35px; 288 | } 289 | 290 | /* IMAGE CAPTIONS */ 291 | 292 | #nodebb-plugin-recent-cards .bx-wrapper .bx-caption { 293 | position: absolute; 294 | bottom: 0; 295 | left: 0; 296 | background: #666\9; 297 | background: rgba(80, 80, 80, 0.75); 298 | width: 100%; 299 | } 300 | 301 | #nodebb-plugin-recent-cards .bx-wrapper .bx-caption span { 302 | color: #fff; 303 | font-family: Arial; 304 | display: block; 305 | font-size: .85em; 306 | padding: 10px; 307 | } 308 | -------------------------------------------------------------------------------- /static/external/bootstrap-grid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2018 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (https://getbootstrap.com/docs/3.3/customize/?id=cd87b217e33874f77fb457b6338264bb) 9 | * Config saved to config.json and https://gist.github.com/cd87b217e33874f77fb457b6338264bb 10 | *//*! 11 | * Bootstrap v3.3.7 (http://getbootstrap.com) 12 | * Copyright 2011-2016 Twitter, Inc. 13 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 14 | *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}} -------------------------------------------------------------------------------- /static/slick/slick.min.js: -------------------------------------------------------------------------------- 1 | (function(factory){"use strict";if(typeof define==="function"&&define.amd){define(["jquery"],factory)}else if(typeof exports!=="undefined"){module.exports=factory(require("jquery"))}else{factory(jQuery)}})(function($){"use strict";var Slick=window.Slick||{};Slick=function(){var instanceUid=0;function Slick(element,settings){var _=this,dataSettings;_.defaults={accessibility:true,adaptiveHeight:false,appendArrows:$(element),appendDots:$(element),arrows:true,asNavFor:null,prevArrow:'',nextArrow:'',autoplay:false,autoplaySpeed:3e3,centerMode:false,centerPadding:"50px",cssEase:"ease",customPaging:function(slider,i){return $('