Acp redesign (#11639)

* acp sidebar

* gap in nav

* remove shadow

* label fixes

* color fixes

* feat: settings page wip

* feat: scroll spy 👓

move social into general, store social in meta.config like other settings
write upgrade script

* remove social

* rermove openapi routes

* cleanup, highlight selected nav item

* more cleanup

* advanced margin top

* derp

* match design

* bring back version alert

fix homepage js, since it moved to general settings

* remove unused tpls

these moved to general settings

* remove more  css

* offcanvas for mobile

fix search

* add timeout

* add new props

* manage categories

* small fixes

* category-edit

* feat category page fixes

* add title to settings pages

add user settings page

* small fixes

* some more settings pages

* fix: plugin page titles

* more settings pages

* more padding

* more pages, add acp paginator.tpl

so it doesn't change when active theme changes

* remove placeholder

* dashboard table

* fix: openapi

* fix: controller tests

* use fonts from core

* some small fixes

* fix rep page

* refactor: fix name of upgrade script

* create category modal

group edit

* group/groups pages

* admins mods

* privs

* uploads

* missing margin

* more acp pages

* more pages

* plugins/rewards/widgets

* wrap rewards

* fix widgets

* fix widget clone button

* fix group acp edit link

* update search dropdown

* remove display block from tbody

* use less css

* remove some derp links

* remove striped tables

* remove p tags from lang files

* update email settings

* Update api.tpl

* move tag-whitelist
isekai-main
Barış Soner Uşaklı 2 years ago committed by GitHub
parent 6d4ab1d0c1
commit c3afe44686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -29,6 +29,8 @@
}, },
"dependencies": { "dependencies": {
"@adactive/bootstrap-tagsinput": "0.8.2", "@adactive/bootstrap-tagsinput": "0.8.2",
"@fontsource/inter": "^4.5.14",
"@fontsource/poppins": "^4.5.10",
"@isaacs/ttlcache": "1.4.0", "@isaacs/ttlcache": "1.4.0",
"@popperjs/core": "2.11.8", "@popperjs/core": "2.11.8",
"ace-builds": "1.22.0", "ace-builds": "1.22.0",
@ -82,7 +84,6 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"logrotate-stream": "0.2.9", "logrotate-stream": "0.2.9",
"lru-cache": "9.1.1", "lru-cache": "9.1.1",
"material-design-lite": "1.3.0",
"mime": "3.0.0", "mime": "3.0.0",
"mkdirp": "3.0.1", "mkdirp": "3.0.1",
"mongodb": "5.5.0", "mongodb": "5.5.0",

@ -4,6 +4,13 @@
"acp-title": "%1 | NodeBB Admin Control Panel", "acp-title": "%1 | NodeBB Admin Control Panel",
"settings-header-contents": "Contents", "settings-header-contents": "Contents",
"changes-saved": "Changes Saved",
"changes-saved-message": "Your changes to the NodeBB configuration have been saved.",
"changes-not-saved": "Changes Not Saved", "changes-not-saved": "Changes Not Saved",
"changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)",
"save-changes": "Save changes",
"min": "Min:",
"max": "Max:",
"view": "View",
"edit": "Edit"
} }

@ -5,7 +5,7 @@
"uptime-seconds": "Uptime in Seconds", "uptime-seconds": "Uptime in Seconds",
"uptime-days": "Uptime in Days", "uptime-days": "Uptime in Days",
"mongo": "Mongo", "mongo": "MongoDB",
"mongo.version": "MongoDB Version", "mongo.version": "MongoDB Version",
"mongo.storage-engine": "Storage Engine", "mongo.storage-engine": "Storage Engine",
"mongo.collections": "Collections", "mongo.collections": "Collections",

@ -26,13 +26,13 @@
"updates": "Updates", "updates": "Updates",
"running-version": "You are running <strong>NodeBB v<span id=\"version\">%1</span></strong>.", "running-version": "You are running <strong>NodeBB v<span id=\"version\">%1</span></strong>.",
"keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.",
"up-to-date": "<p>You are <strong>up-to-date</strong> <i class=\"fa fa-check\"></i></p>", "up-to-date": "You are <strong>up-to-date</strong> <i class=\"fa fa-check\"></i>",
"upgrade-available": "<p>A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.</p>", "upgrade-available": "A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.",
"prerelease-upgrade-available": "<p>This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.</p>", "prerelease-upgrade-available": "This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.",
"prerelease-warning": "<p>This is a <strong>pre-release</strong> version of NodeBB. Unintended bugs may occur. <i class=\"fa fa-exclamation-triangle\"></i></p>", "prerelease-warning": "This is a <strong>pre-release</strong> version of NodeBB. Unintended bugs may occur. <i class=\"fa fa-exclamation-triangle\"></i>",
"fallback-emailer-not-found": "Fallback emailer not found!", "fallback-emailer-not-found": "Fallback emailer not found!",
"running-in-development": "<span>Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.</span>", "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator",
"latest-lookup-failed": "<p>Failed to look up latest available version of NodeBB</p>", "latest-lookup-failed": "Failed to look up latest available version of NodeBB",
"notices": "Notices", "notices": "Notices",
"restart-not-required": "Restart not required", "restart-not-required": "Restart not required",

@ -1,4 +1,5 @@
{ {
"plugins": "Plugins",
"trending": "Trending", "trending": "Trending",
"installed": "Installed", "installed": "Installed",
"active": "Active", "active": "Active",

@ -1,10 +1,12 @@
{ {
"rewards": "Rewards", "rewards": "Rewards",
"add-reward": "Add reward",
"condition-if-users": "If User's", "condition-if-users": "If User's",
"condition-is": "Is:", "condition-is": "Is:",
"condition-then": "Then:", "condition-then": "Then:",
"max-claims": "Amount of times reward is claimable", "max-claims": "Amount of times reward is claimable",
"zero-infinite": "Enter 0 for infinite", "zero-infinite": "Enter 0 for infinite",
"select-reward": "Select reward",
"delete": "Delete", "delete": "Delete",
"enable": "Enable", "enable": "Enable",
"disable": "Disable", "disable": "Disable",

@ -1,4 +1,5 @@
{ {
"widgets": "Widgets",
"available": "Available Widgets", "available": "Available Widgets",
"explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.",
"none-installed": "No widgets found! Activate the widget essentials plugin in the <a href=\"%1\">plugins</a> control panel.", "none-installed": "No widgets found! Activate the widget essentials plugin in the <a href=\"%1\">plugins</a> control panel.",

@ -1,10 +1,11 @@
{ {
"manage-admins-and-mods": "Manage Admins & Mods",
"administrators": "Administrators", "administrators": "Administrators",
"global-moderators": "Global Moderators", "global-moderators": "Global Moderators",
"moderators": "Moderators", "moderators": "Moderators",
"no-global-moderators": "No Global Moderators", "no-global-moderators": "No Global Moderators",
"no-sub-categories": "No subcategories", "no-sub-categories": "No subcategories",
"subcategories": "%1 subcategories", "view-children": "View children (%1)",
"no-moderators": "No Moderators", "no-moderators": "No Moderators",
"add-administrator": "Add Administrator", "add-administrator": "Add Administrator",
"add-global-moderator": "Add Global Moderator", "add-global-moderator": "Add Global Moderator",

@ -1,7 +1,11 @@
{ {
"manage-categories": "Manage Categories",
"add-category": "Add category",
"jump-to": "Jump to...",
"settings": "Category Settings", "settings": "Category Settings",
"edit-category": "Edit Category",
"privileges": "Privileges", "privileges": "Privileges",
"back-to-categories": "Back to categories",
"name": "Category Name", "name": "Category Name",
"description": "Category Description", "description": "Category Description",
"bg-color": "Background Colour", "bg-color": "Background Colour",
@ -15,8 +19,11 @@
"post-queue": "Post queue", "post-queue": "Post queue",
"tag-whitelist": "Tag Whitelist", "tag-whitelist": "Tag Whitelist",
"upload-image": "Upload Image", "upload-image": "Upload Image",
"upload": "Upload",
"select-icon": "Select Icon",
"delete-image": "Remove", "delete-image": "Remove",
"category-image": "Category Image", "category-image": "Category Image",
"image-and-icon": "Image & Icon",
"parent-category": "Parent Category", "parent-category": "Parent Category",
"optional-parent-category": "(Optional) Parent Category", "optional-parent-category": "(Optional) Parent Category",
"top-level": "Top Level", "top-level": "Top Level",
@ -31,6 +38,7 @@
"disable": "Disable", "disable": "Disable",
"edit": "Edit", "edit": "Edit",
"analytics": "Analytics", "analytics": "Analytics",
"view-category": "View category", "view-category": "View category",
"set-order": "Set order", "set-order": "Set order",
"set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.",

@ -1,4 +1,10 @@
{ {
"manage-groups": "Manage groups",
"add-group": "Add group",
"edit-group": "Edit Group",
"back-to-groups": "Back to groups",
"view-group": "View group",
"icon-and-title": "Icon & Title",
"name": "Group Name", "name": "Group Name",
"badge": "Badge", "badge": "Badge",
"properties": "Properties", "properties": "Properties",
@ -10,7 +16,7 @@
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"privileges": "Privileges", "privileges": "Privileges",
"download-csv": "CSV", "members-csv": "Members (CSV)",
"search-placeholder": "Search", "search-placeholder": "Search",
"create": "Create Group", "create": "Create Group",
"description-placeholder": "A short description about your group", "description-placeholder": "A short description about your group",

@ -1,4 +1,6 @@
{ {
"manage-privileges": "Manage Privileges",
"discard-changes": "Discard changes",
"global": "Global", "global": "Global",
"admin": "Admin", "admin": "Admin",
"group-privileges": "Group Privileges", "group-privileges": "Group Privileges",

@ -1,9 +1,11 @@
{ {
"manage-tags": "Manage Tags",
"none": "Your forum does not have any topics with tags yet.", "none": "Your forum does not have any topics with tags yet.",
"bg-color": "Background Colour", "bg-color": "Background Colour",
"text-color": "Text Colour", "text-color": "Text Colour",
"description": "Select tags by clicking or dragging, use <code>CTRL</code> to select multiple tags.", "description": "Select tags by clicking or dragging, use <code>CTRL</code> to select multiple tags.",
"create": "Create Tag", "create": "Create Tag",
"add-tag": "Add tag",
"modify": "Modify Tags", "modify": "Modify Tags",
"rename": "Rename Tags", "rename": "Rename Tags",
"delete": "Delete Selected Tags", "delete": "Delete Selected Tags",

@ -1,4 +1,5 @@
{ {
"manage-uploads": "Manage Uploads",
"upload-file": "Upload File", "upload-file": "Upload File",
"filename": "Filename", "filename": "Filename",
"usage": "Post Usage", "usage": "Post Usage",

@ -1,4 +1,5 @@
{ {
"manage-users": "Manage Users",
"users": "Users", "users": "Users",
"edit": "Actions", "edit": "Actions",
"make-admin": "Make Admin", "make-admin": "Make Admin",

@ -72,7 +72,9 @@
"development/info": "Info", "development/info": "Info",
"rebuild-and-restart-forum": "Rebuild & Restart Forum", "rebuild-and-restart-forum": "Rebuild & Restart Forum",
"rebuild-and-restart": "Rebuild & Restart",
"restart-forum": "Restart Forum", "restart-forum": "Restart Forum",
"restart": "Restart",
"logout": "Log out", "logout": "Log out",
"view-forum": "View Forum", "view-forum": "View Forum",

@ -1,11 +1,13 @@
{ {
"general-settings": "General Settings",
"on-this-page": "On this page:",
"site-settings": "Site Settings", "site-settings": "Site Settings",
"title": "Site Title", "title": "Site Title",
"title.short": "Short Title", "title.short": "Short Title",
"title.short-placeholder": "If no short title is specified, the site title will be used", "title.short-placeholder": "If no short title is specified, the site title will be used",
"title.url": "Title Link URL", "title.url": "Title Link URL",
"title.url-placeholder": "The URL of the site title", "title.url-placeholder": "The URL of the site title",
"title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index. <br> Note: This is not the external URL used in emails, etc. That is set by the <code>url</code> property in config.json", "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index. Note: This is not the external URL used in emails, etc. That is set by the <code>url</code> property in config.json",
"title.name": "Your Community Name", "title.name": "Your Community Name",
"title.show-in-header": "Show Site Title in Header", "title.show-in-header": "Show Site Title in Header",
"browser-title": "Browser Title", "browser-title": "Browser Title",
@ -16,7 +18,7 @@
"description": "Site Description", "description": "Site Description",
"keywords": "Site Keywords", "keywords": "Site Keywords",
"keywords-placeholder": "Keywords describing your community, comma-separated", "keywords-placeholder": "Keywords describing your community, comma-separated",
"logo": "Site Logo", "logo-and-icons": "Site Logo & Icons",
"logo.image": "Image", "logo.image": "Image",
"logo.image-placeholder": "Path to a logo to display on forum header", "logo.image-placeholder": "Path to a logo to display on forum header",
"logo.upload": "Upload", "logo.upload": "Upload",

@ -1,5 +1,6 @@
{ {
"settings": "Settings", "settings": "Settings",
"guest-settings": "Guest settings",
"handles.enabled": "Allow guest handles", "handles.enabled": "Allow guest handles",
"handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"",
"topic-views.enabled": "Allow guests to increase topic view counts", "topic-views.enabled": "Allow guests to increase topic view counts",

@ -1,4 +1,5 @@
{ {
"navigation": "Navigation",
"icon": "Icon:", "icon": "Icon:",
"change-icon": "change", "change-icon": "change",
"route": "Route:", "route": "Route:",

@ -1,4 +1,5 @@
{ {
"general": "General",
"sorting": "Post Sorting", "sorting": "Post Sorting",
"sorting.post-default": "Default Post Sorting", "sorting.post-default": "Default Post Sorting",
"sorting.oldest-to-newest": "Oldest to Newest", "sorting.oldest-to-newest": "Oldest to Newest",
@ -23,10 +24,8 @@
"restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)",
"restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)",
"restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)",
"restrictions.min-title-length": "Minimum Title Length", "restrictions.title-length": "Title Length",
"restrictions.max-title-length": "Maximum Title Length", "restrictions.post-length": "Post Length",
"restrictions.min-post-length": "Minimum Post Length",
"restrictions.max-post-length": "Maximum Post Length",
"restrictions.days-until-stale": "Days until topic is considered stale", "restrictions.days-until-stale": "Days until topic is considered stale",
"restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.",
"timestamp": "Timestamp", "timestamp": "Timestamp",
@ -41,10 +40,9 @@
"teaser.last-reply": "Last &ndash; Show the latest reply, or a \"No replies\" placeholder if no replies", "teaser.last-reply": "Last &ndash; Show the latest reply, or a \"No replies\" placeholder if no replies",
"teaser.first": "First", "teaser.first": "First",
"showPostPreviewsOnHover": "Show a preview of posts when mouse overed", "showPostPreviewsOnHover": "Show a preview of posts when mouse overed",
"unread": "Unread Settings", "unread-and-recent": "Unread & Recent Settings",
"unread.cutoff": "Unread cutoff days", "unread.cutoff": "Unread cutoff days",
"unread.min-track-last": "Minimum posts in topic before tracking last read", "unread.min-track-last": "Minimum posts in topic before tracking last read",
"recent": "Recent Settings",
"recent.max-topics": "Maximum topics on /recent", "recent.max-topics": "Maximum topics on /recent",
"recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page",
"signature": "Signature Settings", "signature": "Signature Settings",

@ -27,5 +27,5 @@
"flags.action-on-resolve": "Do the following when a flag is resolved", "flags.action-on-resolve": "Do the following when a flag is resolved",
"flags.action-on-reject": "Do the following when a flag is rejected", "flags.action-on-reject": "Do the following when a flag is rejected",
"flags.action.nothing": "Do nothing", "flags.action.nothing": "Do nothing",
"flags.action.rescind": "Rescind the notification send to moderators/administrators" "flags.action.rescind": "Rescind the notification sent to moderators/administrators"
} }

@ -1,5 +1,4 @@
{ {
"post-sharing": "Post Sharing", "post-sharing": "Post Sharing",
"info-plugins-additional": "Plugins can add additional networks for sharing posts.", "info-plugins-additional": "Plugins can add additional networks for sharing posts."
"save-success": "Successfully saved Post Sharing Networks!"
} }

@ -3,6 +3,7 @@
"link-to-manage": "Manage Tags", "link-to-manage": "Manage Tags",
"system-tags": "System Tags", "system-tags": "System Tags",
"system-tags-help": "Only privileged users will be able to use these tags.", "system-tags-help": "Only privileged users will be able to use these tags.",
"tags-per-topic": "Tags per topic",
"min-per-topic": "Minimum Tags per Topic", "min-per-topic": "Minimum Tags per Topic",
"max-per-topic": "Maximum Tags per Topic", "max-per-topic": "Maximum Tags per Topic",
"min-length": "Minimum Tag Length", "min-length": "Minimum Tag Length",

@ -59,7 +59,7 @@
"max-about-me-length": "Maximum About Me Length", "max-about-me-length": "Maximum About Me Length",
"terms-of-use": "Forum Terms of Use <small>(Leave blank to disable)</small>", "terms-of-use": "Forum Terms of Use <small>(Leave blank to disable)</small>",
"user-search": "User Search", "user-search": "User Search",
"user-search-results-per-page": "Number of results to display", "user-search-results-per-page": "Number of users to display in search results",
"default-user-settings": "Default User Settings", "default-user-settings": "Default User Settings",
"show-email": "Show email", "show-email": "Show email",
"show-fullname": "Show fullname", "show-fullname": "Show fullname",

@ -24,6 +24,7 @@
"save_changes": "Save Changes", "save_changes": "Save Changes",
"save": "Save", "save": "Save",
"create": "Create",
"cancel": "Cancel", "cancel": "Cancel",
"close": "Close", "close": "Close",

@ -82,14 +82,8 @@ paths:
$ref: 'read/admin/dashboard/searches.yaml' $ref: 'read/admin/dashboard/searches.yaml'
"/api/admin/settings/{term}": "/api/admin/settings/{term}":
$ref: 'read/admin/settings/term.yaml' $ref: 'read/admin/settings/term.yaml'
/api/admin/settings/languages:
$ref: 'read/admin/settings/languages.yaml'
/api/admin/settings/navigation: /api/admin/settings/navigation:
$ref: 'read/admin/settings/navigation.yaml' $ref: 'read/admin/settings/navigation.yaml'
/api/admin/settings/homepage:
$ref: 'read/admin/settings/homepage.yaml'
/api/admin/settings/social:
$ref: 'read/admin/settings/social.yaml'
/api/admin/settings/api: /api/admin/settings/api:
$ref: 'read/admin/settings/api.yaml' $ref: 'read/admin/settings/api.yaml'
/api/admin/settings/email: /api/admin/settings/email:

@ -13,6 +13,8 @@ get:
properties: properties:
categoriesPerPage: categoriesPerPage:
type: number type: number
selectCategoryLabel:
type: string
categoriesTree: categoriesTree:
type: array type: array
items: items:
@ -23,6 +25,8 @@ get:
description: A category identifier description: A category identifier
name: name:
type: string type: string
description:
type: string
disabled: disabled:
type: number type: number
icon: icon:

@ -11,6 +11,8 @@ get:
allOf: allOf:
- type: object - type: object
properties: properties:
title:
type: string
groupsExemptFromMaintenanceMode: groupsExemptFromMaintenanceMode:
type: array type: array
items: items:

@ -11,6 +11,8 @@ get:
allOf: allOf:
- type: object - type: object
properties: properties:
title:
type: string
tokens: tokens:
type: array type: array
items: items:

@ -11,6 +11,8 @@ get:
allOf: allOf:
- type: object - type: object
properties: properties:
title:
type: string
emails: emails:
type: array type: array
items: items:

@ -1,23 +0,0 @@
get:
tags:
- admin
summary: Get homepage settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
routes:
type: array
items:
type: object
properties:
route:
type: string
name:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

@ -1,35 +0,0 @@
get:
tags:
- admin
summary: Get language settings
responses:
"200":
description: A JSON object containing available languages and settings
content:
application/json:
schema:
allOf:
- type: object
properties:
languages:
type: array
items:
type: object
properties:
name:
type: string
description: Localised name of the language
code:
type: string
description: A language code (similar to ISO-639)
dir:
type: string
description: Directionality of the language
enum: [ltr, rtl]
selected:
type: boolean
description: Denotes the currently selected default system language on the forum
autoDetectLang:
type: integer
description: Whether the forum will attempt to guess language based on browser's `Accept-Language` header
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

@ -111,4 +111,6 @@ get:
navigation: navigation:
type: array type: array
description: A clone of `enabled` description: A clone of `enabled`
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

@ -11,6 +11,8 @@ get:
allOf: allOf:
- type: object - type: object
properties: properties:
title:
type: string
groupsExemptFromPostQueue: groupsExemptFromPostQueue:
type: array type: array
items: items:

@ -1,28 +0,0 @@
get:
tags:
- admin
summary: Get post social sharing settings
responses:
"200":
description: "A JSON object containing post social sharing settings"
content:
application/json:
schema:
allOf:
- type: object
properties:
posts:
type: array
items:
type: object
properties:
id:
type: string
name:
type: string
class:
type: string
description: A FontAwesome icon string
activated:
type: boolean
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

@ -17,7 +17,17 @@ get:
schema: schema:
allOf: allOf:
- type: object - type: object
properties: {} properties:
title:
type: string
routes:
type: array
postSharing:
type: array
languages:
type: array
autoDetectLang:
type: number
additionalProperties: additionalProperties:
type: object type: object
description: Most of the settings pages have their values loaded on the client-side, so the settings are not exposed server-side. description: Most of the settings pages have their values loaded on the client-side, so the settings are not exposed server-side.

@ -11,6 +11,8 @@ get:
allOf: allOf:
- type: object - type: object
properties: properties:
title:
type: string
notificationSettings: notificationSettings:
type: array type: array
items: items:

@ -1,6 +1,8 @@
@import "./mixins"; @import "fonts";
@import "mixins";
@import "common";
@import "./header"; @import "sidebar";
@import "./mobile"; @import "./mobile";
@import "./general/dashboard"; @import "./general/dashboard";
@ -12,10 +14,8 @@
@import "./settings/api"; @import "./settings/api";
@import "./appearance/customise"; @import "./appearance/customise";
@import "./extend/plugins"; @import "./extend/plugins";
@import "./extend/rewards";
@import "./extend/widgets"; @import "./extend/widgets";
@import "./advanced/database"; @import "settings";
@import "./settings";
@import "./modules/alerts"; @import "./modules/alerts";
@import "./modules/selectable"; @import "./modules/selectable";
@ -27,24 +27,27 @@ body {
} }
.admin { .admin {
background: #fff; .acp-page-container {
font-size: 14px; max-width: 800px;
margin: 0 auto;
h1 { display: flex;
font-size: 35px; flex-direction: column;
gap: $spacer * 1.5;
padding: $spacer * 1.5;
padding-top: 0;
} }
.form-label, .form-check-label { .acp-page-main-header {
font-weight: 700; background-color: white;
} }
.btn {
border-radius: 0; .settings, .categories, .category, .admins-mods {
hr {
color: $gray-500;
} }
.btn-outline-secondary {
color: $gray-700;
box-shadow: 0 1px 4px rgba(0, 0, 0, .4) !important;
&:hover {
background-color: $gray-200 !important;
} }
.form-control::placeholder, .bootstrap-tagsinput::placeholder {
color: $gray-500 !important;
} }
// .floating-button can either be a container or the button itself // .floating-button can either be a container or the button itself
@ -73,22 +76,6 @@ body {
background: $primary !important; background: $primary !important;
} }
.nodebb-logo {
img {
height: 31px;
margin-top: -8px;
margin-left: -7px;
vertical-align: -43%;
}
@include box-header-font;
color: #fff;
}
#breadcrumbs {
cursor: default;
}
@mixin acp-panel-heading() { @mixin acp-panel-heading() {
padding: 7px 14px; padding: 7px 14px;
border: 0; border: 0;
@ -114,10 +101,6 @@ body {
} }
} }
.nav-header {
@include box-header-font;
}
.icon-container { .icon-container {
.fa-nbb-none { .fa-nbb-none {
border: 1px dotted black; border: 1px dotted black;
@ -141,27 +124,6 @@ body {
} }
} }
.navbar-static-top, .navbar-fixed-top {
box-shadow: 0px -3px 12px rgba(0, 0, 0, 0.5);
}
.navbar-header > .navbar-toggle {
margin-right: 8px;
}
.navbar-nav {
>li {
>#reconnect {
color: $gray-700;
}
>#reconnect:focus, >#reconnect:hover {
color: $gray-700;
background-color: transparent;
}
}
}
#taskbar { #taskbar {
display: none; /* not sure why I have to do this, but it only seems to show up on prod */ display: none; /* not sure why I have to do this, but it only seems to show up on prod */
} }
@ -172,57 +134,21 @@ body {
} }
.bootstrap-tagsinput { .bootstrap-tagsinput {
box-shadow: $input-box-shadow;
width: 100%; width: 100%;
border: 0;
box-shadow: none;
padding-left: 0;
input { input {
width: 100%; font-size: 0.875rem;
margin-left: 1px; width: 64px;
margin-top: 9px; padding: 0;
border-bottom: 1px dotted #ccc !important;
padding-bottom: 5px;
padding-left: 0;
} }
} }
} }
// Allowing text to the right of an image-type brand .dropdown-left .dropdown-menu {
// See: https://github.com/twbs/bootstrap/commit/8e2348e9eda51296eb680192379ab37f10355ca3 --bs-position: start;
.navbar-brand > img {
display: inline-block;
}
.category-settings-form {
h3 {
margin-top: 0;
cursor: pointer;
}
h4 {
cursor: pointer;
}
} }
.dropdown-right .dropdown-menu {
.category-preview {
cursor: pointer;
width: 100%;
height: 100px;
text-align: center;
color: white;
margin-top: 0;
.icon {
width: 30px;
height: 30px;
line-height: 40px;
display: inline-block;
margin: 35px 5px 0 5px;
}
}
.category-dropdown-container.right .category-dropdown-menu {
--bs-position: end; --bs-position: end;
} }
@ -261,10 +187,6 @@ body {
opacity: 0.5; opacity: 0.5;
} }
form small {
color: $gray-700;
}
.caret { .caret {
display: inline-block; display: inline-block;
width: 0; width: 0;

@ -1,6 +0,0 @@
.database-info {
span {
display:inline-block;
width:220px;
}
}

@ -0,0 +1,73 @@
.form-label {
font-weight: 500;
font-size: $font-size-sm;
font-family: $font-family-base;
margin-bottom: 0;
}
.form-text {
font-size: 0.75rem!important;;
font-family: $font-family-base;
}
.tracking-tight { letter-spacing: -0.02em; }
$btn-ghost-hover-color: mix($light, $dark, 90%);
@mixin btn-ghost-base {
display: flex;
align-items: center;
justify-content: center;
gap: ($spacer * 0.5);
border-radius: $border-radius-sm;
border-width: 1px;
border-color: transparent;
background-color: transparent;
padding: ($spacer * 0.25) ($spacer * 0.5);
text-align: left;
--bs-text-opacity: 1;
color: rgb(73 80 87 / var(--bs-text-opacity));
font-family: $font-family-secondary;
cursor: pointer;
&:hover, &.active {
background-color: $btn-ghost-hover-color;
color: rgb(73 80 87 / var(--bs-text-opacity));
text-decoration: none;
}
}
.btn-ghost {
@include btn-ghost-base();
line-height: 1.5rem;
> i {
line-height: 1.5rem;
}
}
.btn-ghost-sm {
@include btn-ghost-base();
font-size: 0.875rem;
line-height: 1.25rem;
> i {
line-height: 1.25rem;
}
}
.btn-outline {
@include btn-ghost-base();
border-color: $border-color;
}
.btn-outline-sm {
@include btn-ghost-base();
border-color: $border-color;
font-size: 0.875rem;
line-height: 1.25rem;
}
.flex-basis-md-200 {
@include media-breakpoint-up(md) {
flex-basis: 200px!important;
}
}

@ -1,24 +1,4 @@
.plugins { .plugins {
padding-left: 0px;
li {
list-style-type: none;
background: rgba(64, 64, 64, 0.05);
padding: 1em;
margin-bottom: 5px;
border-left: 5px solid #08c;
margin-left: -40px;
h2 {
font-size: 16px;
margin: 0;
}
p {
font-size: 12px;
}
}
.plugin-list.ui-sortable { .plugin-list.ui-sortable {
li { li {
cursor: pointer; cursor: pointer;

@ -1,52 +0,0 @@
#rewards {
ul {
list-style-type: none;
padding: 0px;
margin: 0px;
> li {
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
&:last-child {
border-bottom: 0;
}
}
}
.rewards { width: 100%; }
.card {
border-radius: 2px;
border-width: 2px;
color: #333;
&.if-block {
border-color: $primary;
max-width: 33%;
}
&.this-block {
border-color: $warning;
max-width: 33%;
}
&.then-block {
border-color: $success;
max-width: 33%;
}
&.reward-block {
border-color: $success;
background-color: lighten($success, 15%);
color: #fff;
a, select, input { color: #fff; }
select > option { color: #333; }
width: 100%;
min-height: 110px;
}
}
}
.page-admin-rewards {
#new {
bottom: calc(64px + $spacer);
background-color: $success;
}
}

@ -0,0 +1,21 @@
@use "@fontsource/inter/scss/mixins" as Inter;
@use "@fontsource/poppins/scss/mixins" as Poppins;
$weights: $font-weight-light, $font-weight-normal, $font-weight-semibold, $font-weight-bold;
@each $weight in $weights {
@include Inter.fontFace(
$weight: $weight,
$display: fallback,
$fontDir: "./plugins/core/inter"
);
@include Poppins.fontFace(
$weight: $weight,
$display: fallback,
$fontDir: "./plugins/core/poppins"
);
}
.ff-base { font-family: $font-family-base !important; }
.ff-sans { font-family: $font-family-sans-serif !important; }
.ff-secondary { font-family: $font-family-secondary; }

@ -1,159 +0,0 @@
.header {
user-select: none;
position: relative;
background: #333;
width: 100%;
height: 200px;
margin-bottom: 50px;
font-size: 16px;
#main-page-title {
position: absolute;
left: 48px;
bottom: 50px;
color: #aaa;
font-size: 47px;
font-weight: 300;
}
.quick-actions {
position: static;
padding: 15px;
display: flex;
flex-direction: row-reverse;
margin: 0;
> * {
margin-right: 20px;
}
> .menu-button {
margin-right: 0;
padding: 0 5px;
}
.alert {
font-size: 14px;
margin-bottom: 0px;
&.alert-info {
background-color: #eee;
color: #333;
}
}
.dropdown {
margin-right: 0px;
.dropdown-toggle i {
padding: 0 1rem;
}
}
.fa {
line-height: 44px;
font-size: 25px;
}
#user_dropdown {
font-size: 25px;
color: #eee;
i {
margin-top: 12px;
display: block;
}
}
}
#acp-search {
input {
padding: 10px 20px;
width: 250px;
height: 44px;
background-color: rgba(0,0,0,.2);
border-radius: 3px;
box-shadow: none;
transition: .4s ease background-color;
&:focus {
background-color: #eee;
color: #333;
}
}
.dropdown:not(.open) {
&:before {
content: '/';
border: 1px solid $gray-500;
border-radius: 5px;
padding: 0px 6px;
font-size: 12px;
font-weight: 600;
pointer-events: none;
position: absolute;
top: 13px;
left: 1em;
}
&:after {
content: attr(data-text);
position: absolute;
top: 13px;
left: 3em;
font-size: small;
font-weight: 600;
pointer-events: none;
}
input {
color: transparent;
}
}
.search-match {
font-weight: 700;
color: black;
}
}
#main-menu > li {
padding-bottom: 10px;
}
> ul {
list-style-type: none;
padding: 0px;
position: absolute;
bottom: -16px;
left: 50px;
> li {
float: left;
margin-right: 30px;
border-bottom: 4px solid transparent;
transition: border-color 150ms linear;
&:hover {
border-color: darken($primary, 20%);
}
&.active {
border-color: $primary;
}
> a {
color: white;
text-transform: uppercase;
text-decoration: none;
outline: none;
}
}
}
.plugins-menu {
max-height: 50vh;
overflow-y: auto;
}
}

@ -1,51 +1,20 @@
div.categories { div.categories {
ul[data-cid] { ul[data-cid] {
user-select: none;
list-style-type: none;
margin: 0;
padding: 0;
> li > ul > li { > li > ul > li {
margin-left: 4.5rem; padding-left: 3rem;
}
> li > a {
margin-left: 4.5rem;
}
.row {
margin-left: -15px;
margin-right: -15px;
} }
> li li:last-child {
.row {
border-bottom: 0px;
}
}
> li { > li {
margin: 16px 0 24px 0;
&.placeholder { &.placeholder {
border: 1px dashed #2196F3; border: 1px dashed #2196F3;
background-color: #E1F5FE; background-color: #E1F5FE;
width: 100%;
} }
} }
} }
.stats {
display: inline-block;
li {
min-height: 0;
display: inline;
margin: 0 16px 0 0;
left: 0;
}
}
.disabled > .category-row { .disabled > .category-row {
.icon, .title, .description {
.icon, .category-header, .description {
opacity: 0.5; opacity: 0.5;
} }
@ -57,31 +26,12 @@ div.categories {
.toggle { .toggle {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%;
line-height: 24px; line-height: 24px;
text-align: center;
vertical-align: bottom;
background-size: cover;
float: left;
margin-right: 0px;
cursor: pointer; cursor: pointer;
.fa {
font-size: 85%;
}
} }
.information { .information {
cursor: move; cursor: move;
padding-left: 3rem;
}
.category-header {
margin-top: 0;
margin-bottom: 8px;
}
.description {
margin: 0;
} }
.children-placeholder { .children-placeholder {

@ -1,30 +1,5 @@
.group {
[component="groups/members"] {
padding: 0;
tbody {
max-height: 500px;
display: block;
overflow-y: auto;
.member-name {
width: 100%;
}
}
}
#group-icon {
cursor: pointer;
}
}
.groups { .groups {
#group-search {
margin-bottom: 10px;
}
.groups-list { .groups-list {
p {
margin: 0;
}
td { td {
max-width: 350px; max-width: 350px;
} }

@ -1,22 +1,12 @@
.tags { .tags {
.tag-list { .tag-list {
h3 {
min-width: 225px;
}
.tag-row { .tag-row {
padding: 0.5rem;
float: left; float: left;
margin-left: 0.5rem;
.tag-item {
cursor: pointer;
display: inline-block;
font-size: 11px;
}
&.ui-selected { &.ui-selected {
background: lighten($success, 25%); background: lighten($success, 25%);
border-radius: $border-radius-sm;
} }
&.ui-selecting { &.ui-selecting {
@ -24,8 +14,4 @@
} }
} }
} }
.tag-topic-count {
font-size: 14px;
}
} }

@ -1,191 +1,12 @@
#mobile-menu {
display: none;
}
@media (max-width: 991px) { @media (max-width: 991px) {
body { body {
height: 100%; height: 100%;
} overflow-y: scroll;
overflow-x: hidden;
#panel {
background-color: inherit;
min-height: 100%;
}
body, #panel, .slideout-menu {
-webkit-overflow-scrolling: touch;
}
.header {
height: 58px;
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.26);
position: fixed;
top: 0px;
z-index: 5;
#main-page-title {
bottom: -31px;
font-size: 20px;
color: #FFF;
left: 52px;
font-weight: 400;
}
#main-menu {
display: none;
}
}
#mobile-menu {
width: 22px;
background: none;
border: none;
margin-right: 10px;
margin-left: -5px;
outline: none !important;
display: block;
position: absolute;
top: 22px;
left: 22px;
.bar {
width: 100%;
height: 2px;
background: #fff;
margin-bottom: 3px;
border-radius: 10px;
}
}
#menu {
background-color: #1D1F20;
background-image: linear-gradient(145deg, #1D1F20, #404348);
a {
color: #fff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
}
.menu-header-title {
font-weight: 400;
letter-spacing: 0.5px;
margin: 0;
}
.menu-section {
margin: 25px 0;
&.quick-actions {
margin: 0;
.button-group {
display: flex;
justify-content: center;
}
.alert {
border-radius: 0;
.span {
display: block;
}
}
}
}
.menu-section-title {
text-transform: uppercase;
color: #85888d;
font-weight: 200;
font-size: 13px;
letter-spacing: 1px;
padding: 0 20px;
margin:0;
}
.menu-section-list {
padding: 0;
margin: 10px 0;
list-style: none;
a {
display: block;
padding: 10px 20px;
}
a:hover {
background-color: rgba(255, 255, 255, 0.1);
text-decoration: none;
}
}
#panel {
background: white;
min-height: 100%;
padding-top: 80px;
}
.slideout-menu {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 0;
width: 256px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
display: none;
}
.slideout-panel {
position: relative;
}
.slideout-open,
.slideout-open body,
.slideout-open .slideout-panel {
overflow: hidden;
overflow-y: hidden !important;
}
.slideout-open .slideout-menu {
display: block;
} }
html { html {
height: 100%; height: 100%;
overflow-y: hidden; overflow-y: hidden;
} }
.slideout-open {
overflow-y: hidden;
height: 100%;
}
body {
overflow-y: scroll;
overflow-x: hidden;
}
}
@media (max-width: 768px) {
.content-header, .settings-header {
font-size: 200%;
margin-bottom: 20px;
margin-left: -2px;
}
.dropdown-menu {
margin-top: -35px;
margin-right: -2px;
}
} }

@ -1,12 +1,9 @@
#acp-search { .acp-search {
.dropdown-menu { .dropdown-menu {
max-height: 75vh; max-height: 75vh;
overflow-y: auto; overflow-y: auto;
> li > a { > li > a {
// &.focus {
// &:extend(.dropdown-menu>li>a:focus);
// }
&:focus { &:focus {
outline: none; outline: none;
} }

@ -0,0 +1,62 @@
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #6c757d !default;
$gray-700: #495057 !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;
$blue: #0d6efd !default;
$red: #dc3545 !default;
$yellow: #ffc107 !default;
$green: #198754 !default;
$cyan: #0dcaf0 !default;
$body-color: $gray-800;
$text-muted: $gray-600 !default;
// Custom fonts
$font-family-sans-serif: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
$font-family-secondary: "Poppins", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
$font-weight-semibold: 500 !default;
$font-size-base: 1rem !default;
$link-decoration: none;
$link-hover-decoration: underline;
// options
$enable-gradients: false;
// no caret on dropdown-toggle
$enable-caret: false;
// disable smooth scroll, this makes window.scrollTo(0,0) in ajaxify.js take x milliseconds
$enable-smooth-scroll: false;
$enable-shadows: true;
// Buttons
$input-btn-padding-y: .4rem !default;
$input-btn-padding-x: 1rem !default;
// Forms
$input-padding-y: 0.5rem !default;
$input-padding-x: 0.75rem !default;
// $input-padding-y-sm: 0 !default;
// $input-padding-x-sm: 0 !default;
// $input-padding-y-lg: ($font-size-base * 1.25) !default;
// $input-padding-x-lg: 0 !default;
$input-btn-focus-color: #c29ffa80 !default;
$input-border-radius: 0.25rem !default;
$input-focus-border-color: #c29ffa80 !default;
// change inset shadow to outset
$box-shadow-inset: 0 1px 2px rgba($black, .075) !default;

@ -1,20 +0,0 @@
.settings {
.section-content {
border-left: 3px solid $primary;
ul {
list-style-type: none;
font-size: 16px;
padding-left: 20px;
li:not(:last-child) {
margin-bottom: 5px;
}
a {
text-decoration: none;
&:hover{
text-decoration: underline;
}
}
}
}
}

@ -0,0 +1,13 @@
#sidebar-left, #offcanvas {
.btn-ghost, .btn-ghost-sm {
i {
color: $gray-500;
}
}
.accordion-body {
.btn-ghost-sm {
padding-left: 38px!important;
}
}
}

@ -1,45 +0,0 @@
// system font family
// based on those in [bootstrap@5.0.0-alpha1](https://github.com/twbs/bootstrap/blob/b531bda07cbea2e124194aefe3b8597b3ac2578e/scss/_variables.scss#L386)
// and [wordpress admin](https://core.trac.wordpress.org/browser/trunk/src/wp-admin/css/common.css?rev=47835#L220)
// system-ui : supported by the latest browsers for this very purpose
// apple-system, BlinkMacSystemFont : iOS and MacOS
// "Segoe UI" : Windows Vista, 7, 8, 10
// Roboto : Android 4.0+
// Oxygen-Sans : KDE
// Ubuntu : Ubuntu
// Cantarell : GNOME
// "Helvetica Neue" : Mac OS X
// Helvetica : backup, better looking than Arial
// Arial : backup
// "Noto Sans" : broader language support on Android
// sans-serif : whatever the browser can give us
// "Apple Color Emoji" : Emoji on iOS and MacOS
// "Segoe UI Emoji", "Segoe UI Symbol" : Emoji on Windows
// "Noto Color Emoji" : Emoji on Android
$font-family-system: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
$font-family-sans-serif: $font-family-system;
$font-size-base: 0.875rem!default;
$body-color: #666!default;
$light: #f5f5f5;
// options
$enable-gradients: false;
// no caret on dropdown-toggle
$enable-caret: false;
$font-size-base: 1rem !default;
$font-weight-base: 400 !default;
// Buttons
$input-btn-padding-y: .4rem !default;
$input-btn-padding-x: 1rem !default;
// Forms
$input-padding-y: 0.4rem !default;
$input-padding-x: 0 !default;
$input-padding-y-sm: 0 !default;
$input-padding-x-sm: 0 !default;
$input-padding-y-lg: ($font-size-base * 1.25) !default;
$input-padding-x-lg: 0 !default;

@ -42,9 +42,17 @@ app.onDomReady();
} }
require(['hooks'], (hooks) => { require(['hooks'], (hooks) => {
hooks.on('action:ajaxify.end', () => { hooks.on('action:ajaxify.end', (data) => {
updatePageTitle(data.url);
setupRestartLinks();
showCorrectNavTab(); showCorrectNavTab();
startLogoutTimer(); startLogoutTimer();
$('[data-bs-toggle="tooltip"]').tooltip({
animation: false,
container: '#content',
});
if ($('.settings').length) { if ($('.settings').length) {
require(['admin/settings'], function (Settings) { require(['admin/settings'], function (Settings) {
Settings.prepare(); Settings.prepare();
@ -52,21 +60,35 @@ app.onDomReady();
}); });
} }
}); });
hooks.on('action:ajaxify.start', function () {
require(['bootstrap'], function (boostrap) {
const offcanvas = boostrap.Offcanvas.getInstance('#offcanvas');
if (offcanvas) {
offcanvas.hide();
}
});
});
}); });
function showCorrectNavTab() { function showCorrectNavTab() {
// show correct tab if url has # const accordionEl = $('[component="acp/accordion"]');
if (window.location.hash) { let pathname = window.location.pathname;
$('.nav-pills a[href="' + window.location.hash + '"]').tab('show'); if (pathname === '/admin') {
pathname = '/admin/dashboard';
}
const selectedButton = accordionEl.find(`a[href="${pathname}"]`);
if (selectedButton.length) {
accordionEl.find('a').removeClass('active');
accordionEl.find('.accordion-collapse').removeClass('show');
selectedButton.addClass('active');
selectedButton.parents('.accordion-collapse').addClass('show');
} }
} }
$(document).ready(function () { $(document).ready(function () {
if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
require(['admin/modules/search'], function (search) { require(['admin/modules/search'], function (search) {
search.init(); search.init();
}); });
}
$('[component="logout"]').on('click', function () { $('[component="logout"]').on('click', function () {
require(['logout'], function (logout) { require(['logout'], function (logout) {
@ -75,16 +97,25 @@ app.onDomReady();
return false; return false;
}); });
configureSlidemenu();
setupNProgress(); setupNProgress();
fixAccordionIds();
}); });
$(window).on('action:ajaxify.contentLoaded', function (ev, data) { function fixAccordionIds() {
selectMenuItem(data.url); // fix mobile accordion, so it doesn't have same ids as desktop
setupRestartLinks(); // the same accordion partial is used in both places
require('material-design-lite'); const offcanvasAccordion = $('#offcanvas #accordionACP');
componentHandler.upgradeDom(); offcanvasAccordion.attr('id', 'accordionACP-offcanvas');
offcanvasAccordion.find('[data-bs-target]').each((i, el) => {
$(el).attr('data-bs-target', $(el).attr('data-bs-target') + '-offcanvas');
});
offcanvasAccordion.find('[data-bs-parent]').each((i, el) => {
$(el).attr('data-bs-parent', '#accordionACP-offcanvas');
}); });
offcanvasAccordion.find('.accordion-collapse').each((i, el) => {
$(el).attr('id', $(el).attr('id') + '-offcanvas');
});
}
function setupNProgress() { function setupNProgress() {
require(['nprogress', 'hooks'], function (NProgress, hooks) { require(['nprogress', 'hooks'], function (NProgress, hooks) {
@ -98,7 +129,7 @@ app.onDomReady();
}); });
} }
function selectMenuItem(url) { function updatePageTitle(url) {
require(['translator'], function (translator) { require(['translator'], function (translator) {
url = url url = url
.replace(/\/\d+$/, '') .replace(/\/\d+$/, '')
@ -113,17 +144,8 @@ app.onDomReady();
url = [config.relative_path, url].join('/'); url = [config.relative_path, url].join('/');
let fallback; let fallback;
$('#main-menu li').removeClass('active'); $(`[component="acp/accordion"] a[href="${url}"]`).each(function () {
$('#main-menu a').removeClass('active').filter('[href="' + url + '"]').each(function () { fallback = $(this).text();
const menu = $(this);
if (menu.parent().attr('data-link')) {
return;
}
menu
.parent().addClass('active')
.parents('.menu-item').addClass('active');
fallback = menu.text();
}); });
let mainTitle; let mainTitle;
@ -152,9 +174,6 @@ app.onDomReady();
translator.translate(pageTitle, function (title) { translator.translate(pageTitle, function (title) {
document.title = title.replace(/&gt;/g, '>'); document.title = title.replace(/&gt;/g, '>');
}); });
translator.translate(mainTitle, function (text) {
$('#main-page-title').text(text);
});
}); });
} }
@ -164,7 +183,7 @@ app.onDomReady();
// otherwise it can be unloaded when rebuild & restart is run // otherwise it can be unloaded when rebuild & restart is run
// the client can't fetch the template file, resulting in an error // the client can't fetch the template file, resulting in an error
benchpress.render('partials/toast', {}).then(function () { benchpress.render('partials/toast', {}).then(function () {
$('.rebuild-and-restart').off('click').on('click', function () { $('[component="rebuild-and-restart"]').off('click').on('click', function () {
bootbox.confirm('[[admin/admin:alert.confirm-rebuild-and-restart]]', function (confirm) { bootbox.confirm('[[admin/admin:alert.confirm-rebuild-and-restart]]', function (confirm) {
if (confirm) { if (confirm) {
instance.rebuildAndRestart(); instance.rebuildAndRestart();
@ -172,7 +191,7 @@ app.onDomReady();
}); });
}); });
$('.restart').off('click').on('click', function () { $('[component="restart"]').off('click').on('click', function () {
bootbox.confirm('[[admin/admin:alert.confirm-restart]]', function (confirm) { bootbox.confirm('[[admin/admin:alert.confirm-restart]]', function (confirm) {
if (confirm) { if (confirm) {
instance.restart(); instance.restart();
@ -182,62 +201,4 @@ app.onDomReady();
}); });
}); });
} }
function configureSlidemenu() {
require(['slideout'], function (Slideout) {
let env = utils.findBootstrapEnvironment();
const slideout = new Slideout({
panel: document.getElementById('panel'),
menu: document.getElementById('menu'),
padding: 256,
tolerance: 70,
});
if (env === 'md' || env === 'lg') {
slideout.disableTouch();
}
$('#mobile-menu').on('click', function () {
slideout.toggle();
});
$('#menu a').on('click', function () {
slideout.close();
});
$(window).on('resize', function () {
slideout.close();
env = utils.findBootstrapEnvironment();
if (['xxl', 'xl', 'lg'].includes(env)) {
slideout.disableTouch();
$('#header').css({
position: 'relative',
});
} else {
slideout.enableTouch();
$('#header').css({
position: 'fixed',
});
}
});
function onOpeningMenu() {
$('#header').css({
top: ($('#panel').position().top * -1) + 'px',
position: 'absolute',
});
}
slideout.on('open', onOpeningMenu);
slideout.on('close', function () {
$('#header').css({
top: '0px',
position: 'fixed',
});
});
});
}
}()); }());

@ -43,10 +43,6 @@ define('admin/dashboard', [
isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
$('[data-bs-toggle="tooltip"]').tooltip({
animation: false,
});
setupRealtimeButton(); setupRealtimeButton();
setupGraphs(function () { setupGraphs(function () {
socket.emit('admin.rooms.getAll', Admin.updateRoomUsage); socket.emit('admin.rooms.getAll', Admin.updateRoomUsage);
@ -68,19 +64,19 @@ define('admin/dashboard', [
const html = '<div class="text-center float-start">' + const html = '<div class="text-center float-start">' +
'<span class="formatted-number">' + data.onlineRegisteredCount + '</span>' + '<span class="formatted-number">' + data.onlineRegisteredCount + '</span>' +
'<div class="stat">[[admin/dashboard:active-users.users]]</div>' + '<div class="stat text-nowrap">[[admin/dashboard:active-users.users]]</div>' +
'</div>' + '</div>' +
'<div class="text-center float-start">' + '<div class="text-center float-start">' +
'<span class="formatted-number">' + data.onlineGuestCount + '</span>' + '<span class="formatted-number">' + data.onlineGuestCount + '</span>' +
'<div class="stat">[[admin/dashboard:active-users.guests]]</div>' + '<div class="stat text-nowrap">[[admin/dashboard:active-users.guests]]</div>' +
'</div>' + '</div>' +
'<div class="text-center float-start">' + '<div class="text-center float-start">' +
'<span class="formatted-number">' + (data.onlineRegisteredCount + data.onlineGuestCount) + '</span>' + '<span class="formatted-number">' + (data.onlineRegisteredCount + data.onlineGuestCount) + '</span>' +
'<div class="stat">[[admin/dashboard:active-users.total]]</div>' + '<div class="stat text-nowrap">[[admin/dashboard:active-users.total]]</div>' +
'</div>' + '</div>' +
'<div class="text-center float-start">' + '<div class="text-center float-start">' +
'<span class="formatted-number">' + data.socketCount + '</span>' + '<span class="formatted-number">' + data.socketCount + '</span>' +
'<div class="stat">[[admin/dashboard:active-users.connections]]</div>' + '<div class="stat text-nowrap">[[admin/dashboard:active-users.connections]]</div>' +
'</div>'; '</div>';
updateRegisteredGraph(data.onlineRegisteredCount, data.onlineGuestCount); updateRegisteredGraph(data.onlineRegisteredCount, data.onlineGuestCount);

@ -31,7 +31,8 @@ define('admin/extend/plugins', [
pluginsList.on('click', 'button[data-action="toggleActive"]', function () { pluginsList.on('click', 'button[data-action="toggleActive"]', function () {
const pluginEl = $(this).parents('li'); const pluginEl = $(this).parents('li');
pluginID = pluginEl.attr('data-plugin-id'); pluginID = pluginEl.attr('data-plugin-id');
const btn = $('[id="' + pluginID + '"] [data-action="toggleActive"]'); // const btn = $('[id="' + pluginID + '"] [data-action="toggleActive"]');
const btn = $(this);
const pluginData = ajaxify.data.installed[pluginEl.attr('data-plugin-index')]; const pluginData = ajaxify.data.installed[pluginEl.attr('data-plugin-index')];
@ -40,9 +41,8 @@ define('admin/extend/plugins', [
if (err) { if (err) {
return alerts.error(err); return alerts.error(err);
} }
translator.translate('<i class="fa fa-power-off"></i> [[admin/extend/plugins:plugin-item.' + (status.active ? 'deactivate' : 'activate') + ']]', function (buttonText) { btn.siblings('[data-action="toggleActive"]').removeClass('hidden');
btn.html(buttonText); btn.addClass('hidden');
btn.toggleClass('btn-warning', status.active).toggleClass('btn-success', !status.active);
// clone it to active plugins tab // clone it to active plugins tab
if (status.active && !$('#active [id="' + pluginID + '"]').length) { if (status.active && !$('#active [id="' + pluginID + '"]').length) {
@ -65,7 +65,6 @@ define('admin/extend/plugins', [
}, },
}); });
}); });
});
} }
if (pluginData.license && pluginData.active !== true) { if (pluginData.license && pluginData.active !== true) {

@ -95,16 +95,16 @@ define('admin/extend/rewards', ['alerts'], function (alerts) {
html += '<label for="' + input.name + '">' + input.label + '<br />'; html += '<label for="' + input.name + '">' + input.label + '<br />';
switch (input.type) { switch (input.type) {
case 'select': case 'select':
html += '<select class="form-control" name="' + input.name + '">'; html += '<select class="form-select form-select-sm" name="' + input.name + '">';
input.values.forEach(function (value) { input.values.forEach(function (value) {
html += '<option value="' + value.value + '">' + value.name + '</option>'; html += '<option value="' + value.value + '">' + value.name + '</option>';
}); });
break; break;
case 'text': case 'text':
html += '<input type="text" class="form-control" name="' + input.name + '" />'; html += '<input type="text" class="form-control form-control-sm" name="' + input.name + '" />';
break; break;
} }
html += '</label><br />'; html += '</label>';
}); });
div.html(html); div.html(html);

@ -15,12 +15,14 @@ define('admin/manage/categories', [
let sortables; let sortables;
Categories.init = function () { Categories.init = function () {
categorySelector.init($('.category [component="category-selector"]'), { categorySelector.init($('[component="category-selector"]'), {
parentCid: ajaxify.data.selectedCategory ? ajaxify.data.selectedCategory.cid : 0, parentCid: ajaxify.data.selectedCategory ? ajaxify.data.selectedCategory.cid : 0,
onSelect: function (selectedCategory) { onSelect: function (selectedCategory) {
ajaxify.go('/admin/manage/categories' + (selectedCategory.cid ? '?cid=' + selectedCategory.cid : '')); ajaxify.go('/admin/manage/categories' + (selectedCategory.cid ? '?cid=' + selectedCategory.cid : ''));
}, },
cacheList: false,
localCategories: [], localCategories: [],
template: 'admin/partials/category/selector-dropdown-right',
}); });
Categories.render(ajaxify.data.categoriesTree); Categories.render(ajaxify.data.categoriesTree);
@ -74,12 +76,14 @@ define('admin/manage/categories', [
}); });
}); });
$('#collapse-all').on('click', function () { $('#toggle-collapse-all').on('click', function () {
toggleAll(false); const $this = $(this);
}); const isCollapsed = parseInt($this.attr('data-collapsed'), 10) === 1;
toggleAll(isCollapsed);
$('#expand-all').on('click', function () { $this.attr('data-collapsed', isCollapsed ? 0 : 1)
toggleAll(true); .translateText(isCollapsed ?
'[[admin/manage/categories:collapse-all]]' :
'[[admin/manage/categories:expand-all]]');
}); });
function toggleAll(expand) { function toggleAll(expand) {
@ -96,7 +100,7 @@ define('admin/manage/categories', [
message: html, message: html,
buttons: { buttons: {
save: { save: {
label: '[[global:save]]', label: '[[global:create]]',
className: 'btn-primary', className: 'btn-primary',
callback: submit, callback: submit,
}, },
@ -110,6 +114,7 @@ define('admin/manage/categories', [
icon: 'fa-none', icon: 'fa-none',
}, },
], ],
template: 'admin/partials/category/selector-dropdown-left',
}; };
const parentSelector = categorySelector.init(modal.find('#parentCidGroup [component="category-selector"]'), options); const parentSelector = categorySelector.init(modal.find('#parentCidGroup [component="category-selector"]'), options);
const cloneFromSelector = categorySelector.init(modal.find('#cloneFromCidGroup [component="category-selector"]'), options); const cloneFromSelector = categorySelector.init(modal.find('#cloneFromCidGroup [component="category-selector"]'), options);
@ -177,7 +182,7 @@ define('admin/manage/categories', [
}; };
Categories.toggle = function (cids, disabled) { Categories.toggle = function (cids, disabled) {
const listEl = document.querySelector('.categories ul'); const listEl = document.querySelector('.categories [data-cid="0"]');
Promise.all(cids.map(cid => api.put('/categories/' + cid, { Promise.all(cids.map(cid => api.put('/categories/' + cid, {
disabled: disabled ? 1 : 0, disabled: disabled ? 1 : 0,
}).then(() => { }).then(() => {
@ -214,14 +219,14 @@ define('admin/manage/categories', [
if (oldParentCid !== newParentCid) { if (oldParentCid !== newParentCid) {
const toggle = document.querySelector(`.categories li[data-cid="${newParentCid}"] .toggle`); const toggle = document.querySelector(`.categories li[data-cid="${newParentCid}"] .toggle`);
if (toggle) { if (toggle) {
toggle.classList.toggle('hide', false); toggle.classList.toggle('invisible', false);
} }
const children = document.querySelectorAll(`.categories li[data-cid="${oldParentCid}"] ul[data-cid] li[data-cid]`); const children = document.querySelectorAll(`.categories li[data-cid="${oldParentCid}"] ul[data-cid] li[data-cid]`);
if (!children.length) { if (!children.length) {
const toggle = document.querySelector(`.categories li[data-cid="${oldParentCid}"] .toggle`); const toggle = document.querySelector(`.categories li[data-cid="${oldParentCid}"] .toggle`);
if (toggle) { if (toggle) {
toggle.classList.toggle('hide', true); toggle.classList.toggle('invisible', true);
} }
} }
@ -278,7 +283,7 @@ define('admin/manage/categories', [
// Disable expand toggle // Disable expand toggle
if (!categories.length) { if (!categories.length) {
const toggleEl = container.get(0).querySelector('.toggle'); const toggleEl = container.get(0).querySelector('.toggle');
toggleEl.classList.toggle('hide', true); toggleEl.classList.toggle('invisible', true);
} }
// Handle and children categories in this level have // Handle and children categories in this level have

@ -14,23 +14,51 @@ define('admin/manage/category', [
let updateHash = {}; let updateHash = {};
Category.init = function () { Category.init = function () {
$('#category-settings select').each(function () { const categorySettings = $('#category-settings');
const previewEl = $('[component="category/preview"]');
categorySettings.find('select').each(function () {
const $this = $(this); const $this = $(this);
$this.val($this.attr('data-value')); $this.val($this.attr('data-value'));
}); });
categorySelector.init($('[component="category-selector"]'), { // category switcher
categorySelector.init($('[component="settings/main/header"] [component="category-selector"]'), {
onSelect: function (selectedCategory) { onSelect: function (selectedCategory) {
ajaxify.go('admin/manage/categories/' + selectedCategory.cid); ajaxify.go('admin/manage/categories/' + selectedCategory.cid);
}, },
cacheList: false,
showLinks: true, showLinks: true,
template: 'admin/partials/category/selector-dropdown-right',
});
// parent selector
categorySelector.init($('#parent-category-selector [component="category-selector"]'), {
onSelect: function (selectedCategory) {
const parentCidInput = $('#parent-cid');
parentCidInput.val(selectedCategory.cid);
modified(parentCidInput[0]);
},
selectedCategory: ajaxify.data.category.parent, // switch selection to parent
localCategories: [
{
cid: 0,
name: '[[admin/manage/categories:parent-category-none]]',
icon: 'fa-list',
},
],
cacheList: false,
showLinks: true,
template: 'admin/partials/category/selector-dropdown-right',
}); });
handleTags(); handleTags();
$('#category-settings input, #category-settings select, #category-settings textarea').on('change', function (ev) { categorySettings.find('input, select, textarea').on('change', function (ev) {
modified(ev.target); modified(ev.target);
}); });
$('[type="checkbox"]').on('change', function () {
modified($(this));
});
$('[data-name="imageClass"]').on('change', function () { $('[data-name="imageClass"]').on('change', function () {
$('.category-preview').css('background-size', $(this).val()); $('.category-preview').css('background-size', $(this).val());
@ -38,7 +66,6 @@ define('admin/manage/category', [
$('[data-name="bgColor"], [data-name="color"]').on('input', function () { $('[data-name="bgColor"], [data-name="color"]').on('input', function () {
const $inputEl = $(this); const $inputEl = $(this);
const previewEl = $inputEl.parents('[data-cid]').find('.category-preview');
if ($inputEl.attr('data-name') === 'bgColor') { if ($inputEl.attr('data-name') === 'bgColor') {
previewEl.css('background-color', $inputEl.val()); previewEl.css('background-color', $inputEl.val());
} else if ($inputEl.attr('data-name') === 'color') { } else if ($inputEl.attr('data-name') === 'color') {
@ -171,56 +198,41 @@ define('admin/manage/category', [
params: { cid: cid }, params: { cid: cid },
}, function (imageUrlOnServer) { }, function (imageUrlOnServer) {
$('#category-image').val(imageUrlOnServer); $('#category-image').val(imageUrlOnServer);
const previewBox = inputEl.parent().parent().siblings('.category-preview'); previewEl.css('background-image', 'url(' + imageUrlOnServer + '?' + new Date().getTime() + ')');
previewBox.css('background', 'url(' + imageUrlOnServer + '?' + new Date().getTime() + ')');
modified($('#category-image')); modified($('#category-image'));
}); });
}); });
$('#category-image').on('change', function () { $('#category-image').on('change', function () {
$('.category-preview').css('background-image', $(this).val() ? ('url("' + $(this).val() + '")') : ''); previewEl.css('background-image', $(this).val() ? ('url("' + $(this).val() + '")') : '');
modified($('#category-image')); modified($('#category-image'));
}); });
$('.delete-image').on('click', function (e) { $('.delete-image').on('click', function (e) {
e.preventDefault(); e.preventDefault();
const inputEl = $('#category-image'); const inputEl = $('#category-image');
const previewBox = $('.category-preview');
inputEl.val(''); inputEl.val('');
previewBox.css('background-image', ''); previewEl.css('background-image', '');
modified(inputEl[0]); modified(inputEl[0]);
$(this).parent().addClass('hide').hide();
}); });
$('.category-preview').on('click', function () { previewEl.on('click', function () {
iconSelect.init($(this).find('i'), modified); iconSelect.init($(this).find('i'), modified);
}); });
$('[type="checkbox"]').on('change', function () {
modified($(this));
});
$('button[data-action="setParent"], button[data-action="changeParent"]').on('click', Category.launchParentSelector);
$('button[data-action="removeParent"]').on('click', function () {
api.put('/categories/' + ajaxify.data.category.cid, {
parentCid: 0,
}).then(() => {
$('button[data-action="removeParent"]').parent().addClass('hide');
$('button[data-action="changeParent"]').parent().addClass('hide');
$('button[data-action="setParent"]').removeClass('hide');
}).catch(alerts.error);
});
$('button[data-action="toggle"]').on('click', function () { $('button[data-action="toggle"]').on('click', function () {
const $this = $(this); const $this = $(this);
const disabled = $this.attr('data-disabled') === '1'; const disabled = $this.attr('data-disabled') === '1';
api.put('/categories/' + ajaxify.data.category.cid, { api.put('/categories/' + ajaxify.data.category.cid, {
disabled: disabled ? 0 : 1, disabled: disabled ? 0 : 1,
}).then(() => { }).then(() => {
$this.translateText(!disabled ? '[[admin/manage/categories:enable]]' : '[[admin/manage/categories:disable]]'); $this.find('.label').translateText(
$this.toggleClass('btn-primary', !disabled).toggleClass('btn-danger', disabled); !disabled ? '[[admin/manage/categories:enable]]' : '[[admin/manage/categories:disable]]'
);
$this.find('i')
.toggleClass(['fa-check', 'text-success'], !disabled)
.toggleClass(['fa-ban', 'text-danger'], disabled);
$this.attr('data-disabled', disabled ? 0 : 1); $this.attr('data-disabled', disabled ? 0 : 1);
}).catch(alerts.error); }).catch(alerts.error);
}); });
@ -276,30 +288,5 @@ define('admin/manage/category', [
}); });
} }
Category.launchParentSelector = function () {
categorySelector.modal({
onSubmit: function (selectedCategory) {
const parentCid = selectedCategory.cid;
if (!parentCid) {
return;
}
api.put('/categories/' + ajaxify.data.category.cid, {
parentCid: parentCid,
}).then(() => {
api.get(`/categories/${parentCid}`, {}).then(function (parent) {
if (parent && parent.icon && parent.name) {
const buttonHtml = '<i class="fa ' + parent.icon + '"></i> ' + parent.name;
$('button[data-action="changeParent"]').html(buttonHtml).parent().removeClass('hide');
}
});
$('button[data-action="removeParent"]').parent().removeClass('hide');
$('button[data-action="setParent"]').addClass('hide');
}).catch(alerts.error);
},
showLinks: true,
});
};
return Category; return Category;
}); });

@ -47,7 +47,7 @@ define('admin/manage/group', [
setupGroupMembersMenu(); setupGroupMembersMenu();
$('#group-icon, #group-icon-label').on('click', function () { $('#group-icon-container').on('click', function () {
const currentIcon = groupIcon.attr('value'); const currentIcon = groupIcon.attr('value');
iconSelect.init(groupIcon, function () { iconSelect.init(groupIcon, function () {
let newIcon = groupIcon.attr('value'); let newIcon = groupIcon.attr('value');
@ -77,6 +77,7 @@ define('admin/manage/group', [
cids = cids.filter((cid, index, array) => array.indexOf(cid) === index); cids = cids.filter((cid, index, array) => array.indexOf(cid) === index);
$('#memberPostCids').val(cids.join(',')); $('#memberPostCids').val(cids.join(','));
cidSelector.selectCategory(0); cidSelector.selectCategory(0);
return false;
}, },
}); });
@ -87,6 +88,16 @@ define('admin/manage/group', [
app.flags._unsaved = true; app.flags._unsaved = true;
}); });
$('[data-action="delete"]').on('click', function () {
bootbox.confirm('[[admin/manage/groups:alerts.confirm-delete]]', function (confirm) {
if (confirm) {
api.del(`/groups/${slugify(ajaxify.data.group.name)}`, {}).then(() => {
ajaxify.go('/admin/managegroups');
}).catch(alerts.error);
}
});
});
$('#save').on('click', function () { $('#save').on('click', function () {
api.put(`/groups/${slugify(groupName)}`, { api.put(`/groups/${slugify(groupName)}`, {
name: $('#change-group-name').val(), name: $('#change-group-name').val(),

@ -30,22 +30,8 @@ define('admin/manage/groups', [
break; break;
} }
}); });
enableCategorySelectors();
}; };
function enableCategorySelectors() {
$('.groups-list [component="category-selector"]').each(function () {
const nameEncoded = $(this).parents('[data-name-encoded]').attr('data-name-encoded');
categorySelector.init($(this), {
onSelect: function (selectedCategory) {
ajaxify.go('admin/manage/privileges/' + selectedCategory.cid + '?group=' + nameEncoded);
},
showLinks: true,
});
});
}
function handleCreate() { function handleCreate() {
$('#create').on('click', function () { $('#create').on('click', function () {
app.parseAndTranslate('admin/partials/create_group_modal', {}).then((html) => { app.parseAndTranslate('admin/partials/create_group_modal', {}).then((html) => {
@ -119,7 +105,6 @@ define('admin/manage/groups', [
}, function (html) { }, function (html) {
groupsEl.find('[data-groupname]').remove(); groupsEl.find('[data-groupname]').remove();
groupsEl.find('tbody').append(html); groupsEl.find('tbody').append(html);
enableCategorySelectors();
}); });
}); });
} }

@ -21,7 +21,7 @@ define('admin/modules/search', ['mousetrap', 'alerts'], function (mousetrap, ale
// and wrap the match in a `.search-match` element // and wrap the match in a `.search-match` element
.replace( .replace(
new RegExp('^[\\s\\S]*?(.{0,25})(' + escaped + ')(.{0,25})[\\s\\S]*?$', 'gmi'), new RegExp('^[\\s\\S]*?(.{0,25})(' + escaped + ')(.{0,25})[\\s\\S]*?$', 'gmi'),
'...$1<span class="search-match">$2</span>$3...<br>' '...$1<span class="search-match fw-bold">$2</span>$3...<br>'
) )
// collapse whitespace // collapse whitespace
.replace(/(?:\n ?)+/g, '\n') .replace(/(?:\n ?)+/g, '\n')
@ -33,7 +33,7 @@ define('admin/modules/search', ['mousetrap', 'alerts'], function (mousetrap, ale
); );
return '<li role="presentation" class="result">' + return '<li role="presentation" class="result">' +
'<a class="dropdown-item" role="menuitem" href= "' + config.relative_path + '/' + namespace + '" >' + '<a class="dropdown-item rounded-1" role="menuitem" href= "' + config.relative_path + '/' + namespace + '" >' +
title + title +
'<br>' + (!results ? '' : '<br>' + (!results ? '' :
('<small><code>' + ('<small><code>' +
@ -60,11 +60,17 @@ define('admin/modules/search', ['mousetrap', 'alerts'], function (mousetrap, ale
}; };
function setupACPSearch(dict) { function setupACPSearch(dict) {
const dropdown = $('#acp-search .dropdown'); const searchEls = $('[component="acp/search"]');
const menu = $('#acp-search .dropdown-menu'); searchEls.each((index, searchEl) => {
const input = $('#acp-search input'); setupSearch(dict, $(searchEl));
const placeholderText = dropdown.attr('data-text'); });
}
function setupSearch(dict, searchEl) {
const dropdown = searchEl.find('.dropdown');
const menu = searchEl.find('.dropdown-menu');
const input = searchEl.find('input');
const placeholderText = dropdown.attr('data-text');
if (!config.searchEnabled) { if (!config.searchEnabled) {
menu.addClass('search-disabled'); menu.addClass('search-disabled');
} }
@ -73,7 +79,7 @@ define('admin/modules/search', ['mousetrap', 'alerts'], function (mousetrap, ale
dropdown.addClass('open'); dropdown.addClass('open');
}); });
$('#acp-search').parents('form').on('submit', function (ev) { searchEl.parents('form').on('submit', function (ev) {
const query = input.val(); const query = input.val();
const selected = menu.get(0).querySelector('li.result > a.focus') || menu.get(0).querySelector('li.result > a'); const selected = menu.get(0).querySelector('li.result > a.focus') || menu.get(0).querySelector('li.result > a');
const href = selected ? selected.getAttribute('href') : config.relative_path + '/search?in=titlesposts&term=' + escape(query); const href = selected ? selected.getAttribute('href') : config.relative_path + '/search?in=titlesposts&term=' + escape(query);

@ -1,29 +1,55 @@
'use strict'; 'use strict';
define('admin/settings', ['uploader', 'mousetrap', 'hooks', 'alerts', 'settings'], function (uploader, mousetrap, hooks, alerts, settings) { define('admin/settings', [
'uploader', 'mousetrap', 'hooks', 'alerts', 'settings', 'bootstrap',
], function (uploader, mousetrap, hooks, alerts, settings, bootstrap) {
const Settings = {}; const Settings = {};
Settings.populateTOC = function () { Settings.populateTOC = function () {
const headers = $('.settings-header'); const headers = $('.settings-header');
const tocEl = $('[component="settings/toc"]');
const tocList = $('[component="settings/toc/list"]');
const mainHader = $('[component="settings/main/header"]');
if (headers.length > 1) { if (headers.length > 1 && tocList.length) {
headers.each(function () { headers.each(function () {
const header = $(this).text(); const header = $(this).text();
const anchor = header.toLowerCase().replace(/ /g, '-').trim(); const anchor = header.toLowerCase()
.replace(/ /g, '-')
.replace(/&/g, '-')
.trim();
$(this).parent().attr('id', anchor);
tocList.append(`<a class="btn-ghost-sm text-xs justify-content-start text-decoration-none" href="#${anchor}">${header}</a>`);
});
const offset = mainHader.outerHeight(true);
// https://stackoverflow.com/a/11814275/583363
tocList.find('a').on('click', function (event) {
event.preventDefault();
const href = $(this).attr('href');
$(href)[0].scrollIntoView();
window.location.hash = href;
scrollBy(0, -offset);
setTimeout(() => {
tocList.find('a').removeClass('active');
$(this).addClass('active');
}, 10);
return false;
});
$(this).prepend('<a name="' + anchor + '"></a>'); new bootstrap.ScrollSpy($('#spy-container')[0], {
$('.section-content ul').append('<li><a href="#' + anchor + '">' + header + '</a></li>'); target: '#settings-navbar',
rootMargin: '-10% 0px -70%',
smoothScroll: true,
}); });
const scrollTo = $('a[name="' + window.location.hash.replace('#', '') + '"]'); const scrollTo = $(`${window.location.hash}`);
if (scrollTo.length) { if (scrollTo.length) {
$('html, body').animate({ $('html, body').animate({
scrollTop: (scrollTo.offset().top) + 'px', scrollTop: (scrollTo.offset().top - offset) + 'px',
}, 400); }, 400);
} }
} else { tocEl.removeClass('hidden');
$('.content-header').parents('.row').remove();
} }
}; };
@ -117,7 +143,7 @@ define('admin/settings', ['uploader', 'mousetrap', 'hooks', 'alerts', 'settings'
saveBtnEl.classList.toggle('saved', true); saveBtnEl.classList.toggle('saved', true);
setTimeout(() => { setTimeout(() => {
saveBtnEl.classList.toggle('saved', false); saveBtnEl.classList.toggle('saved', false);
}, 5000); }, 2500);
} }
}; };

@ -20,7 +20,19 @@ define('admin/settings/general', ['admin/settings'], function () {
$('button[data-action="removeOgImage"]').on('click', function () { $('button[data-action="removeOgImage"]').on('click', function () {
$('input[data-field="removeOgImage"]').val(''); $('input[data-field="removeOgImage"]').val('');
}); });
$('[data-field="homePageRoute"]').on('change', toggleCustomRoute);
toggleCustomRoute();
}; };
function toggleCustomRoute() {
if ($('[data-field="homePageRoute"]').val() === 'custom') {
$('#homePageCustom').show();
} else {
$('#homePageCustom').hide();
}
}
return Module; return Module;
}); });

@ -1,22 +0,0 @@
'use strict';
define('admin/settings/homepage', ['admin/settings'], function () {
function toggleCustomRoute() {
if ($('[data-field="homePageRoute"]').val() === 'custom') {
$('#homePageCustom').show();
} else {
$('#homePageCustom').hide();
}
}
const Homepage = {};
Homepage.init = function () {
$('[data-field="homePageRoute"]').on('change', toggleCustomRoute);
toggleCustomRoute();
};
return Homepage;
});

@ -163,13 +163,12 @@ define('admin/settings/navigation', [
function toggle() { function toggle() {
const btn = $(this); const btn = $(this);
const disabled = btn.hasClass('btn-success'); const disabled = btn.hasClass('enable');
const index = btn.parents('[data-index]').attr('data-index'); const index = btn.parents('[data-index]').attr('data-index');
translator.translate(disabled ? '[[admin/settings/navigation:btn.disable]]' : '[[admin/settings/navigation:btn.enable]]', function (html) { btn.siblings('.toggle').removeClass('hidden');
btn.toggleClass('btn-warning').toggleClass('btn-success').html(html); btn.addClass('hidden');
btn.parents('li').find('[name="enabled"]').val(disabled ? 'on' : ''); btn.parents('li').find('[name="enabled"]').val(disabled ? 'on' : '');
$('#active-navigation [data-index="' + index + '"] a').toggleClass('text-muted', !disabled); $('#active-navigation [data-index="' + index + '"] a').toggleClass('text-muted', !disabled);
});
return false; return false;
} }

@ -1,27 +0,0 @@
'use strict';
define('admin/settings/social', ['alerts'], function (alerts) {
const social = {};
social.init = function () {
$('#save').on('click', function () {
const networks = [];
$('#postSharingNetworks input[type="checkbox"]').each(function () {
if ($(this).prop('checked')) {
networks.push($(this).attr('id'));
}
});
socket.emit('admin.social.savePostSharingNetworks', networks, function (err) {
if (err) {
return alerts.error(err);
}
alerts.success('[[admin/settings/social:save-success]]');
});
});
};
return social;
});

@ -22,7 +22,8 @@ define('forum/groups/memberlist', ['api', 'bootbox', 'alerts'], function (api, b
title: '[[groups:details.add-member]]', title: '[[groups:details.add-member]]',
message: html, message: html,
buttons: { buttons: {
ok: { OK: {
label: '[[groups:details.add-member]]',
callback: function () { callback: function () {
const users = []; const users = [];
modal.find('[data-uid][data-selected]').each(function (index, el) { modal.find('[data-uid][data-selected]').each(function (index, el) {

@ -9,7 +9,7 @@ define('categoryFilter', ['categorySearch', 'api', 'hooks'], function (categoryS
} }
options = options || {}; options = options || {};
options.states = options.states || ['watching', 'notwatching', 'ignoring']; options.states = options.states || ['watching', 'notwatching', 'ignoring'];
options.template = 'partials/category/filter-dropdown-left'; options.template = options.template || 'partials/category/filter-dropdown-left';
hooks.fire('action:category.filter.options', { el: el, options: options }); hooks.fire('action:category.filter.options', { el: el, options: options });

@ -8,6 +8,7 @@ define('categorySearch', ['alerts', 'bootstrap'], function (alerts, bootstrap) {
options = options || {}; options = options || {};
options.privilege = options.privilege || 'topics:read'; options.privilege = options.privilege || 'topics:read';
options.states = options.states || ['watching', 'notwatching', 'ignoring']; options.states = options.states || ['watching', 'notwatching', 'ignoring'];
options.cacheList = options.hasOwnProperty('cacheList') ? options.cacheList : true;
let localCategories = []; let localCategories = [];
if (Array.isArray(options.localCategories)) { if (Array.isArray(options.localCategories)) {
@ -36,7 +37,7 @@ define('categorySearch', ['alerts', 'bootstrap'], function (alerts, bootstrap) {
const val = searchEl.find('input').val(); const val = searchEl.find('input').val();
if (val.length > 1 || (!val && !categoriesList)) { if (val.length > 1 || (!val && !categoriesList)) {
loadList(val, function (categories) { loadList(val, function (categories) {
categoriesList = categoriesList || categories; categoriesList = options.cacheList && (categoriesList || categories);
renderList(categories); renderList(categories);
}); });
} else if (!val && categoriesList) { } else if (!val && categoriesList) {

@ -1,8 +1,8 @@
'use strict'; 'use strict';
define('categorySelector', [ define('categorySelector', [
'categorySearch', 'bootbox', 'hooks', 'categorySearch', 'bootbox', 'hooks', 'translator',
], function (categorySearch, bootbox, hooks) { ], function (categorySearch, bootbox, hooks, translator) {
const categorySelector = {}; const categorySelector = {};
categorySelector.init = function (el, options) { categorySelector.init = function (el, options) {
@ -13,7 +13,7 @@ define('categorySelector', [
const onSelect = options.onSelect || function () {}; const onSelect = options.onSelect || function () {};
options.states = options.states || ['watching', 'notwatching', 'ignoring']; options.states = options.states || ['watching', 'notwatching', 'ignoring'];
options.template = 'partials/category/selector-dropdown-left'; options.template = options.template || 'partials/category/selector-dropdown-left';
hooks.fire('action:category.selector.options', { el: el, options: options }); hooks.fire('action:category.selector.options', { el: el, options: options });
categorySearch.init(el, options); categorySearch.init(el, options);
@ -22,15 +22,21 @@ define('categorySelector', [
el: el, el: el,
selectedCategory: null, selectedCategory: null,
}; };
el.on('click', '[data-cid]', function () { el.on('click', '[data-cid]', function () {
const categoryEl = $(this); const categoryEl = $(this);
if (categoryEl.hasClass('disabled')) { if (categoryEl.hasClass('disabled')) {
return false; return false;
} }
selector.selectCategory(categoryEl.attr('data-cid')); selector.selectCategory(categoryEl.attr('data-cid'));
onSelect(selector.selectedCategory); return onSelect(selector.selectedCategory);
});
let defaultSelectHtml = selector.el.find('[component="category-selector-selected"]').html();
translator.translate(defaultSelectHtml, (translated) => {
defaultSelectHtml = translated;
}); });
const defaultSelectHtml = selector.el.find('[component="category-selector-selected"]').html();
selector.selectCategory = function (cid) { selector.selectCategory = function (cid) {
const categoryEl = selector.el.find('[data-cid="' + cid + '"]'); const categoryEl = selector.el.find('[data-cid="' + cid + '"]');
selector.selectedCategory = { selector.selectedCategory = {
@ -54,6 +60,14 @@ define('categorySelector', [
selector.getSelectedCid = function () { selector.getSelectedCid = function () {
return selector.selectedCategory ? selector.selectedCategory.cid : 0; return selector.selectedCategory ? selector.selectedCategory.cid : 0;
}; };
if (options.hasOwnProperty('selectedCategory')) {
app.parseAndTranslate(options.template, { selectedCategory: options.selectedCategory }, function (html) {
selector.el.find('[component="category-selector-selected"]').html(
html.find('[component="category-selector-selected"]').html()
);
});
}
return selector; return selector;
}; };

@ -323,7 +323,7 @@ define('iconSelect', ['benchpress', 'bootbox'], function (Benchpress, bootbox) {
} }
icons.remove(); icons.remove();
iconData.forEach((iconData) => { iconData.forEach((iconData) => {
iconContainer.append($(`<i class="fa fa-xl fa-${iconData.style} fa-${iconData.id}" data-label="${iconData.label}"></i>`)); iconContainer.append($(`<i class="fa fa-xl fa-${iconData.style} fa-${iconData.id} rounded-1" data-label="${iconData.label}"></i>`));
}); });
icons = modalEl.find('.nbb-fa-icons i'); icons = modalEl.find('.nbb-fa-icons i');
changeSelection(); changeSelection();

@ -364,6 +364,13 @@ Categories.buildForSelectCategories = function (categories, fields, parentCid) {
const rootCategories = categories.filter(category => category && category.parentCid === parentCid); const rootCategories = categories.filter(category => category && category.parentCid === parentCid);
rootCategories.sort((a, b) => {
if (a.order !== b.order) {
return a.order - b.order;
}
return a.cid - b.cid;
});
rootCategories.forEach(category => recursive(category, categoriesData, '', 0)); rootCategories.forEach(category => recursive(category, categoriesData, '', 0));
const pickFields = [ const pickFields = [

@ -60,8 +60,9 @@ categoriesController.getAll = async function (req, res) {
} }
const fields = [ const fields = [
'cid', 'name', 'icon', 'parentCid', 'disabled', 'link', 'order', 'cid', 'name', 'icon', 'parentCid', 'disabled', 'link',
'color', 'bgColor', 'backgroundImage', 'imageClass', 'subCategoriesPerPage', 'order', 'color', 'bgColor', 'backgroundImage', 'imageClass',
'subCategoriesPerPage', 'description',
]; ];
const categoriesData = await categories.getCategoriesFields(cids, fields); const categoriesData = await categories.getCategoriesFields(cids, fields);
const result = await plugins.hooks.fire('filter:admin.categories.get', { categories: categoriesData, fields: fields }); const result = await plugins.hooks.fire('filter:admin.categories.get', { categories: categoriesData, fields: fields });
@ -101,6 +102,7 @@ categoriesController.getAll = async function (req, res) {
breadcrumbs: crumbs, breadcrumbs: crumbs,
pagination: pagination.create(page, pageCount, req.query), pagination: pagination.create(page, pageCount, req.query),
categoriesPerPage: meta.config.categoriesPerPage, categoriesPerPage: meta.config.categoriesPerPage,
selectCategoryLabel: '[[admin/manage/categories:jump-to]]',
}); });
}; };

@ -18,13 +18,27 @@ const settingsController = module.exports;
settingsController.get = async function (req, res) { settingsController.get = async function (req, res) {
const term = req.params.term || 'general'; const term = req.params.term || 'general';
res.render(`admin/settings/${term}`); const payload = {
title: `[[admin/menu:settings/${term}]]`,
};
if (term === 'general') {
payload.routes = await helpers.getHomePageRoutes(req.uid);
payload.postSharing = await social.getPostSharing();
const languageData = await languages.list();
languageData.forEach((language) => {
language.selected = language.code === meta.config.defaultLang;
});
payload.languages = languageData;
payload.autoDetectLang = meta.config.autoDetectLang;
}
res.render(`admin/settings/${term}`, payload);
}; };
settingsController.email = async (req, res) => { settingsController.email = async (req, res) => {
const emails = await emailer.getTemplates(meta.config); const emails = await emailer.getTemplates(meta.config);
res.render('admin/settings/email', { res.render('admin/settings/email', {
title: '[[admin/menu:settings/email]]',
emails: emails, emails: emails,
sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path), sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path),
services: emailer.listServices(), services: emailer.listServices(),
@ -38,6 +52,7 @@ settingsController.user = async (req, res) => {
label: `[[notifications:${type}]]`, label: `[[notifications:${type}]]`,
})); }));
res.render('admin/settings/user', { res.render('admin/settings/user', {
title: '[[admin/menu:settings/user]]',
notificationSettings: notificationSettings, notificationSettings: notificationSettings,
}); });
}; };
@ -45,6 +60,7 @@ settingsController.user = async (req, res) => {
settingsController.post = async (req, res) => { settingsController.post = async (req, res) => {
const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1); const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1);
res.render('admin/settings/post', { res.render('admin/settings/post', {
title: '[[admin/menu:settings/post]]',
groupsExemptFromPostQueue: groupData, groupsExemptFromPostQueue: groupData,
}); });
}; };
@ -52,22 +68,11 @@ settingsController.post = async (req, res) => {
settingsController.advanced = async (req, res) => { settingsController.advanced = async (req, res) => {
const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1); const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1);
res.render('admin/settings/advanced', { res.render('admin/settings/advanced', {
title: '[[admin/menu:settings/advanced]]',
groupsExemptFromMaintenanceMode: groupData, groupsExemptFromMaintenanceMode: groupData,
}); });
}; };
settingsController.languages = async function (req, res) {
const languageData = await languages.list();
languageData.forEach((language) => {
language.selected = language.code === meta.config.defaultLang;
});
res.render('admin/settings/languages', {
languages: languageData,
autoDetectLang: meta.config.autoDetectLang,
});
};
settingsController.navigation = async function (req, res) { settingsController.navigation = async function (req, res) {
const [admin, allGroups] = await Promise.all([ const [admin, allGroups] = await Promise.all([
navigationAdmin.getAdmin(), navigationAdmin.getAdmin(),
@ -94,23 +99,14 @@ settingsController.navigation = async function (req, res) {
}); });
admin.navigation = admin.enabled.slice(); admin.navigation = admin.enabled.slice();
admin.title = '[[admin/menu:settings/navigation]]';
res.render('admin/settings/navigation', admin); res.render('admin/settings/navigation', admin);
}; };
settingsController.homepage = async function (req, res) {
const routes = await helpers.getHomePageRoutes(req.uid);
res.render('admin/settings/homepage', { routes: routes });
};
settingsController.social = async function (req, res) {
const posts = await social.getPostSharing();
res.render('admin/settings/social', {
posts: posts,
});
};
settingsController.api = async (req, res) => { settingsController.api = async (req, res) => {
const tokens = await api.utils.tokens.list(); const tokens = await api.utils.tokens.list();
res.render('admin/settings/api', { tokens }); res.render('admin/settings/api', {
title: '[[admin/menu:settings/api]]',
tokens,
});
}; };

@ -31,10 +31,8 @@ const buildImports = {
}, },
admin: function (source) { admin: function (source) {
return [ return [
'@import "admin/vars";', '@import "admin/overrides";',
'@import "bootswatch/dist/materia/variables";',
'@import "bootstrap/scss/bootstrap";', '@import "bootstrap/scss/bootstrap";',
'@import "bootswatch/dist/materia/bootswatch";',
'@import "mixins";', '@import "mixins";',
'@import "fontawesome";', '@import "fontawesome";',
'@import "@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput";', '@import "@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput";',

@ -83,6 +83,10 @@ JS.buildModules = async function () {
JS.linkStatics = async function () { JS.linkStatics = async function () {
await fs.promises.rm(path.join(__dirname, '../../build/public/plugins'), { recursive: true, force: true }); await fs.promises.rm(path.join(__dirname, '../../build/public/plugins'), { recursive: true, force: true });
plugins.staticDirs['core/inter'] = path.join(basePath, 'node_modules//@fontsource/inter/files');
plugins.staticDirs['core/poppins'] = path.join(basePath, 'node_modules//@fontsource/poppins/files');
await Promise.all(Object.keys(plugins.staticDirs).map(async (mappedPath) => { await Promise.all(Object.keys(plugins.staticDirs).map(async (mappedPath) => {
const sourceDir = plugins.staticDirs[mappedPath]; const sourceDir = plugins.staticDirs[mappedPath];
const destDir = path.join(__dirname, '../../build/public/plugins', mappedPath); const destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);

@ -142,6 +142,14 @@ module.exports = function (Posts) {
filePaths = [filePaths]; filePaths = [filePaths];
} }
if (process.platform === 'win32') {
// windows path => 'files\\1685368788211-1-profileimg.jpg'
// turn it into => 'files/1685368788211-1-profileimg.jpg'
filePaths.forEach((file) => {
file.path = file.path.split(path.sep).join(path.posix.sep);
});
}
const keys = filePaths.map(fileObj => `upload:${md5(fileObj.path.replace('-resized', ''))}:pids`); const keys = filePaths.map(fileObj => `upload:${md5(fileObj.path.replace('-resized', ''))}:pids`);
return await Promise.all(keys.map(k => db.getSortedSetRange(k, 0, -1))); return await Promise.all(keys.map(k => db.getSortedSetRange(k, 0, -1)));
}; };

@ -35,10 +35,7 @@ module.exports = function (app, name, middleware, controllers) {
helpers.setupAdminPageRoute(app, `/${name}/settings/user`, middlewares, controllers.admin.settings.user); helpers.setupAdminPageRoute(app, `/${name}/settings/user`, middlewares, controllers.admin.settings.user);
helpers.setupAdminPageRoute(app, `/${name}/settings/post`, middlewares, controllers.admin.settings.post); helpers.setupAdminPageRoute(app, `/${name}/settings/post`, middlewares, controllers.admin.settings.post);
helpers.setupAdminPageRoute(app, `/${name}/settings/advanced`, middlewares, controllers.admin.settings.advanced); helpers.setupAdminPageRoute(app, `/${name}/settings/advanced`, middlewares, controllers.admin.settings.advanced);
helpers.setupAdminPageRoute(app, `/${name}/settings/languages`, middlewares, controllers.admin.settings.languages);
helpers.setupAdminPageRoute(app, `/${name}/settings/navigation`, middlewares, controllers.admin.settings.navigation); helpers.setupAdminPageRoute(app, `/${name}/settings/navigation`, middlewares, controllers.admin.settings.navigation);
helpers.setupAdminPageRoute(app, `/${name}/settings/homepage`, middlewares, controllers.admin.settings.homepage);
helpers.setupAdminPageRoute(app, `/${name}/settings/social`, middlewares, controllers.admin.settings.social);
helpers.setupAdminPageRoute(app, `/${name}/settings/api`, middlewares, controllers.admin.settings.api); helpers.setupAdminPageRoute(app, `/${name}/settings/api`, middlewares, controllers.admin.settings.api);
helpers.setupAdminPageRoute(app, `/${name}/settings/:term?`, middlewares, controllers.admin.settings.get); helpers.setupAdminPageRoute(app, `/${name}/settings/:term?`, middlewares, controllers.admin.settings.get);

@ -3,6 +3,7 @@
const _ = require('lodash'); const _ = require('lodash');
const plugins = require('./plugins'); const plugins = require('./plugins');
const db = require('./database'); const db = require('./database');
const meta = require('./meta');
const social = module.exports; const social = module.exports;
@ -26,9 +27,8 @@ social.getPostSharing = async function () {
}, },
]; ];
networks = await plugins.hooks.fire('filter:social.posts', networks); networks = await plugins.hooks.fire('filter:social.posts', networks);
const activated = await db.getSetMembers('social:posts.activated');
networks.forEach((network) => { networks.forEach((network) => {
network.activated = activated.includes(network.id); network.activated = parseInt(meta.config[`post-sharing-${network.id}`], 10) === 1;
}); });
social.postSharing = networks; social.postSharing = networks;
@ -41,12 +41,16 @@ social.getActivePostSharing = async function () {
}; };
social.setActivePostSharingNetworks = async function (networkIDs) { social.setActivePostSharingNetworks = async function (networkIDs) {
// keeping for 1.0.0 upgrade script that uses this function
social.postSharing = null; social.postSharing = null;
await db.delete('social:posts.activated');
if (!networkIDs.length) { if (!networkIDs.length) {
return; return;
} }
await db.setAdd('social:posts.activated', networkIDs); const data = {};
networkIDs.forEach((id) => {
data[`post-sharing-${id}`] = 1;
});
await db.setObject('config', data);
}; };
require('./promisify')(social); require('./promisify')(social);

@ -19,7 +19,6 @@ SocketAdmin.tags = require('./admin/tags');
SocketAdmin.rewards = require('./admin/rewards'); SocketAdmin.rewards = require('./admin/rewards');
SocketAdmin.navigation = require('./admin/navigation'); SocketAdmin.navigation = require('./admin/navigation');
SocketAdmin.rooms = require('./admin/rooms'); SocketAdmin.rooms = require('./admin/rooms');
SocketAdmin.social = require('./admin/social');
SocketAdmin.themes = require('./admin/themes'); SocketAdmin.themes = require('./admin/themes');
SocketAdmin.plugins = require('./admin/plugins'); SocketAdmin.plugins = require('./admin/plugins');
SocketAdmin.widgets = require('./admin/widgets'); SocketAdmin.widgets = require('./admin/widgets');

@ -1,9 +0,0 @@
'use strict';
const social = require('../../social');
const SocketSocial = module.exports;
SocketSocial.savePostSharingNetworks = async function (socket, data) {
await social.setActivePostSharingNetworks(data);
};

@ -0,0 +1,19 @@
'use strict';
const db = require('../../database');
module.exports = {
name: 'Migrate post sharing values to config',
timestamp: Date.UTC(2023, 4, 23),
method: async () => {
const activated = await db.getSetMembers('social:posts.activated');
if (activated.length) {
const data = {};
activated.forEach((id) => {
data[`post-sharing-${id}`] = 1;
});
await db.setObject('config', data);
await db.delete('social:posts.activated');
}
},
};

@ -1,9 +1,9 @@
<!-- IMPORT admin/partials/settings/header.tpl -->
<div class="row post-cache"> <div class="row post-cache settings px-lg-4">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="row"> <div class="row">
{{{each caches}}} {{{each caches}}}
<div class="col-lg-3"> <div class="col-xl-3">
<div class="card"> <div class="card">
<div class="card-header">[[admin/advanced/cache:{@key}-cache]]</div> <div class="card-header">[[admin/advanced/cache:{@key}-cache]]</div>
<div class="card-body"> <div class="card-body">
@ -52,4 +52,4 @@
</div> </div>
</div> </div>
<!-- IMPORT admin/partials/settings/footer.tpl --> <!-- IMPORT admin/partials/save_button.tpl -->

@ -1,7 +1,7 @@
<div class="acp-page-container">
<div class="row database"> <div class="row database">
{{{ if mongo }}} {{{ if mongo }}}
<div class="col-lg-6"> <div class="{{{ if redis }}}col-lg-6{{{ else }}}col-lg-12{{{ end }}}">
{{{ if mongo.serverStatusError }}} {{{ if mongo.serverStatusError }}}
<div class="alert alert-warning"> <div class="alert alert-warning">
{mongo.serverStatusError} {mongo.serverStatusError}
@ -11,28 +11,28 @@
<div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:mongo]]</div> <div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:mongo]]</div>
<div class="card-body"> <div class="card-body">
<div class="database-info"> <div class="database-info">
<span>[[admin/advanced/database:mongo.version]]</span> <span class="text-end">{mongo.version}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.version]]</span> <span class="text-end">{mongo.version}</span></div>
<hr/> <hr/>
<span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{mongo.uptime}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{mongo.uptime}</span></div>
<span>[[admin/advanced/database:mongo.storage-engine]]</span> <span class="text-end">{mongo.storageEngine}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.storage-engine]]</span> <span class="text-end">{mongo.storageEngine}</span></div>
<span>[[admin/advanced/database:mongo.collections]]</span> <span class="text-end formatted-number">{mongo.collections}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.collections]]</span> <span class="text-end formatted-number">{mongo.collections}</span></div>
<span>[[admin/advanced/database:mongo.objects]]</span> <span class="text-end formatted-number">{mongo.objects}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.objects]]</span> <span class="text-end formatted-number">{mongo.objects}</span></div>
<span>[[admin/advanced/database:mongo.avg-object-size]]</span> <span class="text-end">[[admin/advanced/database:x-b, {mongo.avgObjSize}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.avg-object-size]]</span> <span class="text-end">[[admin/advanced/database:x-b, {mongo.avgObjSize}]]</span></div>
<hr/> <hr/>
<span>[[admin/advanced/database:mongo.data-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.dataSize}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.data-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.dataSize}]]</span></div>
<span>[[admin/advanced/database:mongo.storage-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.storageSize}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.storage-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.storageSize}]]</span></div>
<span>[[admin/advanced/database:mongo.index-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.indexSize}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.index-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.indexSize}]]</span></div>
{{{ if mongo.fileSize }}} {{{ if mongo.fileSize }}}
<span>[[admin/advanced/database:mongo.file-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.fileSize}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.file-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.fileSize}]]</span></div>
{{{ end }}} {{{ end }}}
<hr/> <hr/>
<span>[[admin/advanced/database:mongo.resident-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.resident}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.resident-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.resident}]]</span></div>
<span>[[admin/advanced/database:mongo.virtual-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.virtual}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.virtual-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.virtual}]]</span></div>
<span>[[admin/advanced/database:mongo.mapped-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.mapped}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.mapped-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.mapped}]]</span></div>
<hr/> <hr/>
<span>[[admin/advanced/database:mongo.bytes-in]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.network.bytesIn}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.bytes-in]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.network.bytesIn}]]</span></div>
<span>[[admin/advanced/database:mongo.bytes-out]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.network.bytesOut}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.bytes-out]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.network.bytesOut}]]</span></div>
<span>[[admin/advanced/database:mongo.num-requests]]</span> <span class="text-end">{mongo.network.numRequests}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.num-requests]]</span> <span class="text-end">{mongo.network.numRequests}</span></div>
</div> </div>
</div> </div>
</div> </div>
@ -45,34 +45,34 @@
<div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:redis]]</div> <div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:redis]]</div>
<div class="card-body"> <div class="card-body">
<div class="database-info"> <div class="database-info">
<span>[[admin/advanced/database:redis.version]]</span> <span class="text-end">{redis.redis_version}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.version]]</span> <span class="text-end">{redis.redis_version}</span></div>
<hr/> <hr/>
<span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{redis.uptime_in_seconds}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{redis.uptime_in_seconds}</span></div>
<span>[[admin/advanced/database:uptime-days]]</span> <span class="text-end">{redis.uptime_in_days}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:uptime-days]]</span> <span class="text-end">{redis.uptime_in_days}</span></div>
<hr/> <hr/>
<span>[[admin/advanced/database:redis.keys]]</span> <span class="text-end formatted-number">{redis.keys}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.keys]]</span> <span class="text-end formatted-number">{redis.keys}</span></div>
<span>[[admin/advanced/database:redis.expires]]</span> <span class="text-end formatted-number">{redis.expires}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.expires]]</span> <span class="text-end formatted-number">{redis.expires}</span></div>
<span>[[admin/advanced/database:redis.avg-ttl]]</span> <span class="text-end formatted-number">{redis.avg_ttl}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.avg-ttl]]</span> <span class="text-end formatted-number">{redis.avg_ttl}</span></div>
<span>[[admin/advanced/database:redis.connected-clients]]</span> <span class="text-end">{redis.connected_clients}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.connected-clients]]</span> <span class="text-end">{redis.connected_clients}</span></div>
<span>[[admin/advanced/database:redis.connected-slaves]]</span> <span class="text-end">{redis.connected_slaves}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.connected-slaves]]</span> <span class="text-end">{redis.connected_slaves}</span></div>
<span>[[admin/advanced/database:redis.blocked-clients]]</span> <span class="text-end">{redis.blocked_clients}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.blocked-clients]]</span> <span class="text-end">{redis.blocked_clients}</span></div>
<hr/> <hr/>
<span>[[admin/advanced/database:redis.used-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.used_memory_human}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.used-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.used_memory_human}]]</span></div>
<span>[[admin/advanced/database:redis.memory-frag-ratio]]</span> <span class="text-end">{redis.mem_fragmentation_ratio}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.memory-frag-ratio]]</span> <span class="text-end">{redis.mem_fragmentation_ratio}</span></div>
<hr/> <hr/>
<span>[[admin/advanced/database:redis.total-connections-recieved]]</span> <span class="text-end formatted-number">{redis.total_connections_received}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.total-connections-recieved]]</span> <span class="text-end formatted-number">{redis.total_connections_received}</span></div>
<span>[[admin/advanced/database:redis.total-commands-processed]]</span> <span class="text-end formatted-number">{redis.total_commands_processed}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.total-commands-processed]]</span> <span class="text-end formatted-number">{redis.total_commands_processed}</span></div>
<span>[[admin/advanced/database:redis.iops]]</span> <span class="text-end formatted-number">{redis.instantaneous_ops_per_sec}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.iops]]</span> <span class="text-end formatted-number">{redis.instantaneous_ops_per_sec}</span></div>
<span>[[admin/advanced/database:redis.iinput]]</span> <span class="text-end">[[admin/advanced/database:x-mb, {redis.instantaneous_input}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.iinput]]</span> <span class="text-end">[[admin/advanced/database:x-mb, {redis.instantaneous_input}]]</span></div>
<span>[[admin/advanced/database:redis.ioutput]]</span> <span class="text-end">[[admin/advanced/database:x-mb, {redis.instantaneous_output}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.ioutput]]</span> <span class="text-end">[[admin/advanced/database:x-mb, {redis.instantaneous_output}]]</span></div>
<span>[[admin/advanced/database:redis.total-input]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.total_net_input}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.total-input]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.total_net_input}]]</span></div>
<span>[[admin/advanced/database:redis.total-output]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.total_net_output}]]</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.total-output]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.total_net_output}]]</span></div>
<hr/> <hr/>
<span>[[admin/advanced/database:redis.keyspace-hits]]</span> <span class="text-end formatted-number">{redis.keyspace_hits}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.keyspace-hits]]</span> <span class="text-end formatted-number">{redis.keyspace_hits}</span></div>
<span>[[admin/advanced/database:redis.keyspace-misses]]</span> <span class="text-end formatted-number">{redis.keyspace_misses}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.keyspace-misses]]</span> <span class="text-end formatted-number">{redis.keyspace_misses}</span></div>
</div> </div>
</div> </div>
</div> </div>
@ -85,9 +85,9 @@
<div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:postgres]]</div> <div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:postgres]]</div>
<div class="card-body"> <div class="card-body">
<div class="database-info"> <div class="database-info">
<span>[[admin/advanced/database:postgres.version]]</span> <span class="text-end">{postgres.version}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:postgres.version]]</span> <span class="text-end">{postgres.version}</span></div>
<hr/> <hr/>
<span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{postgres.uptime}</span><br/> <div class="d-flex justify-content-between"><span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{postgres.uptime}</span></div>
</div> </div>
</div> </div>
</div> </div>
@ -95,9 +95,9 @@
{{{ end }}} {{{ end }}}
</div> </div>
<div class="row database"> <div class="row">
{{{ if mongo }}} {{{ if mongo }}}
<div class="col-lg-6"> <div class="{{{ if redis }}}col-lg-6{{{ else }}}col-lg-12{{{ end }}}">
<div class="card"> <div class="card">
<h5 class="card-header" data-bs-toggle="collapse" data-bs-target=".mongodb-raw"> <h5 class="card-header" data-bs-toggle="collapse" data-bs-target=".mongodb-raw">
<i class="fa fa-caret-down"></i> [[admin/advanced/database:mongo.raw-info]] <i class="fa fa-caret-down"></i> [[admin/advanced/database:mongo.raw-info]]
@ -144,3 +144,4 @@
</div> </div>
{{{ end }}} {{{ end }}}
</div> </div>
</div>

@ -1,3 +1,4 @@
<div class="px-lg-4">
<div class="row"> <div class="row">
<div class="col-lg-9"> <div class="col-lg-9">
<div class="row"> <div class="row">
@ -49,7 +50,7 @@
<i class="fa fa-exclamation-triangle"></i> [[admin/advanced/errors:error.404]] <i class="fa fa-exclamation-triangle"></i> [[admin/advanced/errors:error.404]]
</div> </div>
<div class="card-body"> <div class="card-body">
<table class="table table-striped"> <table class="table">
<thead> <thead>
<th>[[admin/advanced/errors:route]]</th> <th>[[admin/advanced/errors:route]]</th>
<th>[[admin/advanced/errors:count]]</th> <th>[[admin/advanced/errors:count]]</th>
@ -76,3 +77,4 @@
</div> </div>
</div> </div>
</div> </div>
</div>

@ -1,4 +1,4 @@
<div class="row events"> <div class="row events px-lg-4">
<div class="col-lg-9"> <div class="col-lg-9">
<h5><i class="fa fa-calendar-o"></i> [[admin/advanced/events:events]]</h5> <h5><i class="fa fa-calendar-o"></i> [[admin/advanced/events:events]]</h5>
{{{ if !events.length }}} {{{ if !events.length }}}
@ -6,25 +6,27 @@
{{{ end }}} {{{ end }}}
<div class="events-list"> <div class="events-list">
{{{ each events }}} {{{ each events }}}
<div class="card mb-3"> <div class="card mb-3" data-eid="{events.eid}">
<div class="card-body"> <div class="card-body">
<div data-eid="{events.eid}"> <div class="mb-3 d-flex flex-wrap justify-content-between align-items-center gap-1">
<div class="mb-3"> <div>
<span class="badge bg-primary">#{events.eid}</span> <span class="badge bg-primary">#{events.eid}</span>
<span class="badge bg-info">{events.type}</span> <span class="badge bg-info">{events.type}</span>
<span class="badge bg-info">uid {events.uid}</span> <span class="badge bg-info">uid {events.uid}</span>
{{{ if events.ip }}}<span class="badge bg-info">{events.ip}</span>{{{ end }}} {{{ if events.ip }}}<span class="badge bg-info">{events.ip}</span>{{{ end }}}
<a href="{config.relative_path}/user/{events.user.userslug}" target="_blank">{buildAvatar(events.user, "24px", true)}</a> <a href="{config.relative_path}/user/{events.user.userslug}" target="_blank">{buildAvatar(events.user, "24px", true)}</a>
<a href="{config.relative_path}/user/{events.user.userslug}" target="_blank">{events.user.username}</a> <a href="{config.relative_path}/user/{events.user.userslug}" target="_blank">{events.user.username}</a>
<span class="float-end delete-event ms-2 pointer"><i class="fa fa-trash-o"></i></span> <span class="text-xs">{events.timestampISO}</span>
<span class="float-end">{events.timestampISO}</span>
</div> </div>
<pre class="text-bg-light p-3">{events.jsonString}</pre> <div>
<button class="btn btn-light btn-sm delete-event ms-2 pointer"><i class="fa fa-trash-o text-danger"></i></button>
</div> </div>
</div> </div>
<pre class="text-bg-light p-3">{events.jsonString}</pre>
</div>
</div> </div>
{{{ end }}} {{{ end }}}
<!-- IMPORT partials/paginator.tpl --> <!-- IMPORT admin/partials/paginator.tpl -->
</div> </div>
</div> </div>
<div class="col-lg-3 acp-sidebar"> <div class="col-lg-3 acp-sidebar">

@ -1,9 +1,9 @@
<div class="hooks-list panel-group" id="accordion" role="tablist" aria-multiselectable="true"> <div class="hooks-list panel-group px-lg-4" id="accordion" role="tablist" aria-multiselectable="true">
{{{ each hooks }}} {{{ each hooks }}}
<div class="card"> <div class="card">
<div class="card-header" role="tab"> <div class="card-header" role="tab">
<h6 class="mb-0"> <h6 class="mb-0">
<a style="text-transform: none;" role="button" data-bs-toggle="collapse" data-bs-parent="#accordion" data-bs-target="#{hooks.index}" aria-expanded="true" aria-controls="{hooks.index}">{hooks.hookName}</a> <a style="text-transform: none;" class="text-reset text-decoration-none" role="button" data-bs-toggle="collapse" data-bs-parent="#accordion" data-bs-target="#{hooks.index}" aria-expanded="true" aria-controls="{hooks.index}">{hooks.hookName}</a>
<span class="float-end">{hooks.count} hooks</span> <span class="float-end">{hooks.count} hooks</span>
</h6> </h6>
</div> </div>

@ -1,4 +1,4 @@
<div class="row logs"> <div class="row logs px-lg-4">
<div class="col-lg-9"> <div class="col-lg-9">
<div class="card"> <div class="card">
<div class="card-header"><i class="fa fa-file-text-o"></i> [[admin/advanced/logs:logs]]</div> <div class="card-header"><i class="fa fa-file-text-o"></i> [[admin/advanced/logs:logs]]</div>

@ -1,4 +1,4 @@
<div id="customise" class="customise"> <div id="customise" class="customise px-lg-4">
<ul class="nav nav-pills mb-3"> <ul class="nav nav-pills mb-3">
<li class="nav-item"><a class="nav-link active" href="#custom-css" data-bs-toggle="tab">[[admin/appearance/customise:custom-css]]</a></li> <li class="nav-item"><a class="nav-link active" href="#custom-css" data-bs-toggle="tab">[[admin/appearance/customise:custom-css]]</a></li>
<li class="nav-item"><a class="nav-link" href="#custom-js" data-bs-toggle="tab">[[admin/appearance/customise:custom-js]]</a></li> <li class="nav-item"><a class="nav-link" href="#custom-js" data-bs-toggle="tab">[[admin/appearance/customise:custom-js]]</a></li>

@ -1,4 +1,4 @@
<div id="skins" class="skins"> <div id="skins" class="skins px-lg-4">
<div class="directory row text-center" id="bootstrap_themes"> <div class="directory row text-center" id="bootstrap_themes">
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]] <i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]]
</div> </div>

@ -1,4 +1,4 @@
<div id="themes" class="themes"> <div id="themes" class="themes px-lg-4">
<div class="directory row text-center" id="installed_themes"> <div class="directory row text-center" id="installed_themes">
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/themes:checking-for-installed]] <i class="fa fa-refresh fa-spin"></i> [[admin/appearance/themes:checking-for-installed]]
</div> </div>

@ -1,4 +1,4 @@
<div class="row dashboard"> <div class="row dashboard px-lg-4">
<div class="col-lg-9"> <div class="col-lg-9">
<!-- IMPORT admin/partials/dashboard/graph.tpl --> <!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl --> <!-- IMPORT admin/partials/dashboard/stats.tpl -->
@ -70,11 +70,11 @@
<div class="card-header">[[admin/dashboard:control-panel]]</div> <div class="card-header">[[admin/dashboard:control-panel]]</div>
<div class="card-body text-center"> <div class="card-body text-center">
<div class="d-grid gap-2 mb-2"> <div class="d-grid gap-2 mb-2">
<button class="btn btn-block btn-warning restart"{{{ if !canRestart }}} disabled{{{ end }}}>[[admin/dashboard:restart]]</button> <button component="restart" class="btn btn-block btn-warning btn-sm"{{{ if !canRestart }}} disabled{{{ end }}}>[[admin/dashboard:restart]]</button>
<button class="btn btn-block btn-danger rebuild-and-restart"{{{ if !canRestart }}} disabled{{{ end }}}>[[admin/dashboard:rebuild-and-restart]]</button> <button component="rebuild-and-restart" class="btn btn-block btn-danger btn-sm"{{{ if !canRestart }}} disabled{{{ end }}}>[[admin/dashboard:rebuild-and-restart]]</button>
</div> </div>
{{{ if lastrestart }}} {{{ if lastrestart }}}
<p> <p class="text-sm">
[[admin/dashboard:last-restarted-by]]<br /> [[admin/dashboard:last-restarted-by]]<br />
<a href="{config.relative_path}/uid/{lastrestart.uid}"><span class="badge bg-info">{lastrestart.user.username}</span></a> <span class="timeago" title="{lastrestart.timestampISO}"></span> <a href="{config.relative_path}/uid/{lastrestart.uid}"><span class="badge bg-info">{lastrestart.user.username}</span></a> <span class="timeago" title="{lastrestart.timestampISO}"></span>
</p> </p>
@ -87,7 +87,7 @@
{{{ end }}} {{{ end }}}
</p> </p>
<p> <p>
<a href="{config.relative_path}/admin/settings/advanced" class="btn btn-info btn-block" data-bs-placement="bottom" data-bs-toggle="tooltip" title="[[admin/dashboard:maintenance-mode-title]]">[[admin/dashboard:maintenance-mode]]</a> <a href="{config.relative_path}/admin/settings/advanced" class="btn btn-info btn-block btn-sm" data-bs-placement="bottom" data-bs-toggle="tooltip" title="[[admin/dashboard:maintenance-mode-title]]">[[admin/dashboard:maintenance-mode]]</a>
</p> </p>
<hr /> <hr />
@ -106,9 +106,9 @@
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header">[[admin/dashboard:updates]]</div> <div class="card-header">[[admin/dashboard:updates]]</div>
<div class="card-body"> <div class="card-body">
<div class="alert {{{ if lookupFailed }}}alert-danger{{{ else }}}{{{ if upgradeAvailable }}}alert-warning{{{ else }}}{{{ if currentPrerelease }}}alert-info{{{ else }}}alert-success{{{ end }}}{{{ end }}}{{{ end }}} version-check"> <div class="text-sm alert {{{ if lookupFailed }}}alert-danger{{{ else }}}{{{ if upgradeAvailable }}}alert-warning{{{ else }}}{{{ if currentPrerelease }}}alert-info{{{ else }}}alert-success{{{ end }}}{{{ end }}}{{{ end }}} version-check">
<p>[[admin/dashboard:running-version, {version}]]</p> <p class="">[[admin/dashboard:running-version, {version}]]</p>
<p> <p class="mb-0">
{{{ if lookupFailed }}} {{{ if lookupFailed }}}
[[admin/dashboard:latest-lookup-failed]] [[admin/dashboard:latest-lookup-failed]]
{{{ else }}} {{{ else }}}
@ -128,7 +128,7 @@
{{{ end }}} {{{ end }}}
</p> </p>
</div> </div>
<p> <p class="text-sm">
[[admin/dashboard:keep-updated]] [[admin/dashboard:keep-updated]]
</p> </p>
</div> </div>

@ -1,15 +1,10 @@
<div class="row dashboard"> <div class="row dashboard px-lg-4">
<div class="col-12"> <div class="col-12">
<a class="btn btn-primary mb-3" href="{config.relative_path}/admin/dashboard">
<i class="fa fa-chevron-left"></i>
[[admin/dashboard:back-to-dashboard]]
</a>
<!-- IMPORT admin/partials/dashboard/graph.tpl --> <!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl --> <!-- IMPORT admin/partials/dashboard/stats.tpl -->
<div class="alert alert-info">[[admin/dashboard:details.logins-static, {loginDays}]]</div> <div class="alert alert-info">[[admin/dashboard:details.logins-static, {loginDays}]]</div>
<table class="table table-striped"> <table class="table">
<thead> <thead>
<th class="text-muted">[[admin/manage/users:users.username]]</th> <th class="text-muted">[[admin/manage/users:users.username]]</th>
<th data-sort="joindate">[[admin/dashboard:details.logins-login-time]]</th> <th data-sort="joindate">[[admin/dashboard:details.logins-login-time]]</th>

@ -1,26 +1,22 @@
<div class="row dashboard"> <div class="row dashboard px-lg-4">
<div class="col-12"> <div class="col-12">
<div class="d-flex gap-2 mb-3"> <div class="d-flex justify-content-end gap-2 mb-3">
<a class="btn btn-primary me-auto align-items-center" href="{config.relative_path}/admin/dashboard">
<i class="fa fa-chevron-left"></i>
[[admin/dashboard:back-to-dashboard]]
</a>
<form class="row row-cols-lg-auto g-3 align-items-center" method="GET"> <form class="row row-cols-lg-auto g-3 align-items-center" method="GET">
<div class="col-12 d-flex align-items-center gap-2"> <div class="col-12 d-flex align-items-center gap-2">
<label class="form-label mb-0" for="start">[[admin/dashboard:start]]</label> <label class="form-label mb-0" for="start">[[admin/dashboard:start]]</label>
<input type="date" class="form-control" id="start" name="start" value="{startDate}"> <input type="date" class="form-control form-control-sm" id="start" name="start" value="{startDate}">
</div> </div>
<div class="col-12 d-flex align-items-center gap-2"> <div class="col-12 d-flex align-items-center gap-2">
<label class="form-label mb-0" for="end">[[admin/dashboard:end]]</label> <label class="form-label mb-0" for="end">[[admin/dashboard:end]]</label>
<input type="date" class="form-control" id="end" name="end" value="{endDate}"> <input type="date" class="form-control form-control-sm" id="end" name="end" value="{endDate}">
</div> </div>
<div class="col-12"> <div class="col-12">
<button onclick="$('form').submit();return false;"class="btn btn-primary" type="submit">Filter</button> <button onclick="$('form').submit();return false;"class="btn btn-primary btn-sm" type="submit">Filter</button>
</div> </div>
</form> </form>
</div> </div>
<table class="table table-sm table-striped search-list"> <table class="table table-sm search-list">
<thead> <thead>
<th class="text-end">Count</th> <th class="text-end">Count</th>
<th>Term</th> <th>Term</th>

@ -1,14 +1,9 @@
<div class="row dashboard"> <div class="row dashboard px-lg-4">
<div class="col-12"> <div class="col-12">
<a class="btn btn-primary mb-3" href="{config.relative_path}/admin/dashboard">
<i class="fa fa-chevron-left"></i>
[[admin/dashboard:back-to-dashboard]]
</a>
<!-- IMPORT admin/partials/dashboard/graph.tpl --> <!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl --> <!-- IMPORT admin/partials/dashboard/stats.tpl -->
<table class="table table-striped topics-list"> <table class="table topics-list">
<tbody> <tbody>
{{{ if !topics.length}}} {{{ if !topics.length}}}
<tr> <tr>

@ -1,14 +1,9 @@
<div class="dashboard"> <div class="dashboard px-lg-4">
<div class="col-12"> <div class="col-12">
<a class="btn btn-primary mb-3" href="{config.relative_path}/admin/dashboard">
<i class="fa fa-chevron-left"></i>
[[admin/dashboard:back-to-dashboard]]
</a>
<!-- IMPORT admin/partials/dashboard/graph.tpl --> <!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl --> <!-- IMPORT admin/partials/dashboard/stats.tpl -->
<table class="table table-striped users-list"> <table class="table users-list">
<thead> <thead>
<th class="text-muted">[[admin/manage/users:users.uid]]</th> <th class="text-muted">[[admin/manage/users:users.uid]]</th>
<th class="text-muted">[[admin/manage/users:users.username]]</th> <th class="text-muted">[[admin/manage/users:users.username]]</th>

@ -7,23 +7,23 @@
<div class="card-body"> <div class="card-body">
<span>[[admin/development/info:nodes-responded, {nodeCount}, {timeout}]]</span> <span>[[admin/development/info:nodes-responded, {nodeCount}, {timeout}]]</span>
<table class="table table-striped"> <table class="table table-sm text-sm">
<thead> <thead>
<tr> <tr>
<td>[[admin/development/info:host]]</td> <td class="fw-bold">[[admin/development/info:host]]</td>
<td class="text-center">[[admin/development/info:primary]]</td> <td class="fw-bold text-center">[[admin/development/info:primary]]</td>
<td>[[admin/development/info:pid]]</td> <td class="fw-bold">[[admin/development/info:pid]]</td>
<td>[[admin/development/info:nodejs]]</td> <td class="fw-bold">[[admin/development/info:nodejs]]</td>
<td>[[admin/development/info:online]]</td> <td class="fw-bold">[[admin/development/info:online]]</td>
<td>[[admin/development/info:git]]</td> <td class="fw-bold">[[admin/development/info:git]]</td>
<td>[[admin/development/info:cpu-usage]]</td> <td class="fw-bold">[[admin/development/info:cpu-usage]]</td>
<td>[[admin/development/info:process-memory]]</td> <td class="fw-bold">[[admin/development/info:process-memory]]</td>
<td>[[admin/development/info:system-memory]]</td> <td class="fw-bold">[[admin/development/info:system-memory]]</td>
<td>[[admin/development/info:load]]</td> <td class="fw-bold">[[admin/development/info:load]]</td>
<td>[[admin/development/info:uptime]]</td> <td class="fw-bold">[[admin/development/info:uptime]]</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="text-xs">
{{{ each info }}} {{{ each info }}}
<tr> <tr>
<td>{info.os.hostname}:{info.process.port}</td> <td>{info.os.hostname}:{info.process.port}</td>

@ -1,5 +1,4 @@
<!-- IMPORT admin/partials/settings/header.tpl --> <div class="row logger settings px-lg-4">
<div class="row logger">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="card"> <div class="card">
<div class="card-header">[[admin/development/logger:logger-settings]]</div> <div class="card-header">[[admin/development/logger:logger-settings]]</div>
@ -34,4 +33,4 @@
</div> </div>
</div> </div>
<!-- IMPORT admin/partials/settings/footer.tpl --> <!-- IMPORT admin/partials/save_button.tpl -->

@ -1,3 +1,15 @@
<div class="tags d-flex flex-column gap-2 px-lg-4">
<div class="d-flex border-bottom py-2 m-0 sticky-top acp-page-main-header align-items-center justify-content-between flex-wrap gap-2">
<div class="">
<h4 class="fw-bold tracking-tight mb-0">[[admin/extend/plugins:plugins]]</h4>
</div>
<div class="d-flex align-items-center gap-1">
<input autofocus class="form-control form-control-sm" type="text" id="plugin-search" placeholder="[[admin/extend/plugins:plugin-search-placeholder]]"/><br/>
<button class="btn btn-primary btn-sm text-nowrap" id="plugin-order">[[admin/extend/plugins:order-active]]</button>
</div>
</div>
<div class="">
{{{ if !canChangeState }}} {{{ if !canChangeState }}}
<div class="alert alert-warning">[[error:plugins-set-in-configuration]]</div> <div class="alert alert-warning">[[error:plugins-set-in-configuration]]</div>
{{{ end }}} {{{ end }}}
@ -11,25 +23,25 @@
<li class="nav-item"> <li class="nav-item">
<button class="nav-link active" data-bs-target="#installed" data-bs-toggle="tab"> <button class="nav-link active" data-bs-target="#installed" data-bs-toggle="tab">
[[admin/extend/plugins:installed]] [[admin/extend/plugins:installed]]
<span class="badge bg-light">{installedCount}</span> <span class="badge text-bg-light">{installedCount}</span>
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-target="#active" data-bs-toggle="tab"> <button class="nav-link" data-bs-target="#active" data-bs-toggle="tab">
[[admin/extend/plugins:active]] [[admin/extend/plugins:active]]
<span class="badge bg-light">{activeCount}</span> <span class="badge text-bg-light">{activeCount}</span>
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-target="#deactive" data-bs-toggle="tab"> <button class="nav-link" data-bs-target="#deactive" data-bs-toggle="tab">
[[admin/extend/plugins:inactive]] [[admin/extend/plugins:inactive]]
<span class="badge bg-light">{inactiveCount}</span> <span class="badge text-bg-light">{inactiveCount}</span>
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-target="#upgrade" data-bs-toggle="tab"> <button class="nav-link" data-bs-target="#upgrade" data-bs-toggle="tab">
[[admin/extend/plugins:out-of-date]] [[admin/extend/plugins:out-of-date]]
<span class="badge bg-light">{upgradeCount}</span> <span class="badge text-bg-light">{upgradeCount}</span>
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
@ -37,12 +49,12 @@
</li> </li>
</ul> </ul>
<div class="plugins row"> <div class="plugins row px-2">
<div class="col-lg-9"> <div class="col-lg-9">
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane fade" id="trending"> <div class="tab-pane fade" id="trending">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl --> <!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="trending"> <ul class="trending list-unstyled">
{{{ each trending }}} {{{ each trending }}}
<!-- IMPORT admin/partials/installed_plugin_item.tpl --> <!-- IMPORT admin/partials/installed_plugin_item.tpl -->
{{{ end }}} {{{ end }}}
@ -50,7 +62,7 @@
</div> </div>
<div class="tab-pane fade show active" id="installed"> <div class="tab-pane fade show active" id="installed">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl --> <!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="installed"> <ul class="installed list-unstyled">
{{{ each installed }}} {{{ each installed }}}
<!-- IMPORT admin/partials/installed_plugin_item.tpl --> <!-- IMPORT admin/partials/installed_plugin_item.tpl -->
{{{ end }}} {{{ end }}}
@ -58,19 +70,19 @@
</div> </div>
<div class="tab-pane fade" id="active"> <div class="tab-pane fade" id="active">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl --> <!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="active"></ul> <ul class="active list-unstyled"></ul>
</div> </div>
<div class="tab-pane fade" id="deactive"> <div class="tab-pane fade" id="deactive">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl --> <!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="deactive"></ul> <ul class="deactive list-unstyled"></ul>
</div> </div>
<div class="tab-pane fade" id="upgrade"> <div class="tab-pane fade" id="upgrade">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl --> <!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="upgrade"></ul> <ul class="upgrade list-unstyled"></ul>
</div> </div>
<div class="tab-pane fade" id="download"> <div class="tab-pane fade" id="download">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl --> <!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="download"> <ul class="download list-unstyled">
{{{ each download }}} {{{ each download }}}
<!-- IMPORT admin/partials/download_plugin_item.tpl --> <!-- IMPORT admin/partials/download_plugin_item.tpl -->
{{{ end }}} {{{ end }}}
@ -80,38 +92,22 @@
</div> </div>
<div class="acp-sidebar col-lg-3"> <div class="acp-sidebar col-lg-3">
<div class="card">
<div class="card-header">[[admin/extend/plugins:plugin-search]]</div>
<div class="card-body">
<input autofocus class="form-control" type="text" id="plugin-search" placeholder="[[admin/extend/plugins:plugin-search-placeholder]]"/><br/>
</div>
</div>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="form-check"> <div class="form-check form-switch text-sm">
<input id="plugin-submit-usage" class="form-check-input" type="checkbox" data-field="submitPluginUsage" {{{ if submitPluginUsage }}}checked{{{ end }}}/> <input id="plugin-submit-usage" class="form-check-input" type="checkbox" data-field="submitPluginUsage" {{{ if submitPluginUsage }}}checked{{{ end }}}/>
<label for="plugin-submit-usage" class="form-check-label">[[admin/extend/plugins:submit-anonymous-usage]]</label> <label for="plugin-submit-usage" class="form-check-label">[[admin/extend/plugins:submit-anonymous-usage]]</label>
</div> </div>
</div> <hr/>
</div> <div>
<div class="fw-semibold text-sm">[[admin/extend/plugins:dev-interested]]</div>
<div class="card"> <p class="text-xs text-muted">
<div class="card-header">[[admin/extend/plugins:reorder-plugins]]</div>
<div class="card-body d-grid">
<button class="btn btn-outline-secondary" id="plugin-order">[[admin/extend/plugins:order-active]]</button>
</div>
</div>
<div class="card">
<div class="card-header">[[admin/extend/plugins:dev-interested]]</div>
<div class="card-body">
<p>
[[admin/extend/plugins:docs-info]] [[admin/extend/plugins:docs-info]]
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="modal fade" id="order-active-plugins-modal"> <div class="modal fade" id="order-active-plugins-modal">
<div class="modal-dialog"> <div class="modal-dialog">
@ -137,5 +133,5 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>

@ -1,80 +1,81 @@
<div id="rewards"> <div class="tags d-flex flex-column gap-2 px-lg-4">
<ul id="active"> <div class="d-flex border-bottom py-2 m-0 sticky-top acp-page-main-header align-items-center justify-content-between flex-wrap gap-2">
<div class="">
<h4 class="fw-bold tracking-tight mb-0">[[admin/extend/rewards:rewards]]</h4>
</div>
<div class="d-flex align-items-center gap-1">
<button id="new" class="btn btn-light btn-sm text-nowrap" type="button">
<i class="fa fa-fw fa-plus"></i> [[admin/extend/rewards:add-reward]]
</button>
<button id="save" class="btn btn-primary btn-sm fw-semibold ff-secondary w-100 text-center text-nowrap">[[admin/admin:save-changes]]</button>
</div>
</div>
<div id="rewards" class="">
<ul id="active" class="list-unstyled p-0 m-0">
{{{ each active }}} {{{ each active }}}
<li data-rid="{active.rid}" data-id="{active.id}"> <li data-rid="{active.rid}" data-id="{active.id}">
<div class="row"> <div class="d-flex gap-1 mb-3 flex-wrap">
<div class="col-12 col-lg-8"> <form class="main d-flex gap-1">
<form class="main d-inline-block"> <div class="card card-body m-0 if-block border-info border border-2">
<div class="card card-body d-inline-block if-block">
<label class="form-label" for="condition-if-users">[[admin/extend/rewards:condition-if-users]]</label> <label class="form-label" for="condition-if-users">[[admin/extend/rewards:condition-if-users]]</label>
<select id="condition-if-users" class="form-select" name="condition" data-selected="{active.condition}"> <select id="condition-if-users" class="form-select form-select-sm" name="condition" data-selected="{active.condition}">
{{{ each conditions }}} {{{ each conditions }}}
<option value="{conditions.condition}">{conditions.name}</option> <option value="{conditions.condition}">{conditions.name}</option>
{{{ end }}} {{{ end }}}
</select> </select>
</div> </div>
<div class="card card-body d-inline-block this-block"> <div class="card card-body m-0 this-block border-warning border border-2">
<label class="form-label" for="condition-is">[[admin/extend/rewards:condition-is]]</label> <label class="form-label" for="condition-is">[[admin/extend/rewards:condition-is]]</label>
<div class="row"> <div class="d-flex gap-1 flex-nowrap">
<div class="col-4"> <select id="condition-is" class="form-select form-select-sm" name="conditional" data-selected="{active.conditional}" style="max-width: 64px;">
<select id="condition-is" class="form-select" name="conditional" data-selected="{active.conditional}">
{{{ each conditionals }}} {{{ each conditionals }}}
<option value="{conditionals.conditional}">{conditionals.name}</option> <option value="{conditionals.conditional}">{conditionals.name}</option>
{{{ end }}} {{{ end }}}
</select> </select>
</div>
<div class="col-8"> <input class="form-control form-control-sm" type="text" name="value" value="{active.value}" style="max-width: 64px;"/>
<input class="form-control" type="text" name="value" value="{active.value}" />
</div>
</div> </div>
</div> </div>
<div class="card card-body d-inline-block then-block"> <div class="card card-body m-0 then-block border-primary border border-2">
<label class="form-label" for="condition-then">[[admin/extend/rewards:condition-then]]</label> <label class="form-label" for="condition-then">[[admin/extend/rewards:condition-then]]</label>
<select id="condition-then" class="form-select" name="rid" data-selected="{active.rid}"> <select id="condition-then" class="form-select form-select-sm" name="rid" data-selected="{active.rid}">
{{{ each ../../rewards }}} {{{ each ../../rewards }}}
<option value="{rewards.rid}">{rewards.name}</option> <option value="{rewards.rid}">{rewards.name}</option>
{{{ end }}} {{{ end }}}
</select> </select>
</div> </div>
</form> </form>
</div> <form class="rewards">
<div class="col-12 col-lg-4"> <div class="inputs card card-body m-0 h-100 reward-block border-success border border-2"><div class="d-flex h-100 align-items-center">[[admin/extend/rewards:select-reward]]</div></div>
<form class="rewards d-inline-block">
<div class="inputs card card-body d-inline-block reward-block"></div>
</form> </form>
</div> </div>
<div class="d-flex justify-content-between align-items-center">
<form class="main d-flex gap-1 align-items-start gap-2">
<div class="d-flex flex-column gap-0">
<label class="form-label" for="claimable">[[admin/extend/rewards:max-claims]]</label>
<p class="form-text mb-0">
[[admin/extend/rewards:zero-infinite]]
</p>
</div> </div>
<input id="claimable" class="form-control form-control-sm" type="text" name="claimable" value="{active.claimable}" placeholder="1" style="max-width: 64px;"/>
<div class="float-start">
<div class="card-body d-inline-block">
<form class="main">
<label class="form-label" for="claimable">[[admin/extend/rewards:max-claims]] <small>[[admin/extend/rewards:zero-infinite]]</small></label>
<input id="claimable" class="form-control" type="text" name="claimable" value="{active.claimable}" placeholder="1" />
</form> </form>
</div> <div class="">
</div>
<div class="float-end"> <button class="btn btn-light btn-sm toggle disable {{{ if active.disabled }}}hidden{{{ end }}}"><i class="fa fa-ban text-danger"></i> [[admin/extend/rewards:disable]]</button>
<div class="card-body d-inline-block">
<button class="btn btn-danger delete">[[admin/extend/rewards:delete]]</button> <button class="btn btn-light btn-sm toggle enable {{{ if !active.disabled }}}hidden{{{ end }}}"><i class="fa fa-check text-success"></i> [[admin/extend/rewards:enable]]</button>
{{{ if active.disabled }}}
<button class="btn btn-success toggle">[[admin/extend/rewards:enable]]</button> <button class="btn btn-light btn-sm delete"><i class="fa fa-trash text-danger"></i> [[admin/extend/rewards:delete]]</button>
{{{ else }}}
<button class="btn btn-warning toggle">[[admin/extend/rewards:disable]]</button>
{{{ end }}}
</div> </div>
</div> </div>
<div class="clearfix"></div>
<hr/>
</li> </li>
{{{ end }}} {{{ end }}}
</ul> </ul>
</div> </div>
<div class="floating-button">
<button id="new" class="btn btn-primary position-fixed end-0 px-3 py-2 mb-4 me-4 rounded-circle fs-4" type="button" style="width: 64px; height: 64px;">
<i class="fa fa-fw fa-plus"></i>
</button>
<!-- IMPORT admin/partials/save_button.tpl -->
</div> </div>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save