Compare commits

...

10 Commits

Author SHA1 Message Date
d1aafd77ca
real initial commit 2025-04-12 19:23:06 -04:00
c1b132e8af
real initial commit 2025-04-12 19:16:34 -04:00
Julian Lam
26ffa901f8 fix: apply nodebb/nodebb-theme-harmony@f40603ea5c
- Removes chat header widget area and adds topic breadcrumb option to quickstart theme

cc @barisusakli
2024-09-03 12:58:26 -04:00
Barış Soner Uşaklı
9e2cc7d51b update theme to resolved missing fonts
also add harmony client side modules
2024-01-04 12:59:04 -05:00
Barış Soner Uşaklı
7c41ac2006 closes #3, add theme areas from harmony 2024-01-01 18:42:27 -05:00
Barış Soner Uşaklı
8f488a681d fix: #11 2024-01-01 18:32:24 -05:00
Barış Soner Uşaklı
aed5971d90
Update theme.json 2024-01-01 18:19:00 -05:00
Barış Soner Uşaklı
3d3127ae32
Update package.json 2024-01-01 18:14:49 -05:00
Barış Soner Uşaklı
c1fff76fcc
Update plugin.json 2024-01-01 18:13:22 -05:00
Barış Soner Uşaklı
5e42af990c
Update overrides.scss 2023-10-06 18:01:26 -04:00
30 changed files with 1734 additions and 29 deletions

12
lib/controllers.js Normal file
View File

@ -0,0 +1,12 @@
'use strict';
const accountHelpers = require.main.require('./src/controllers/accounts/helpers');
const helpers = require.main.require('./src/controllers/helpers');
const Controllers = module.exports;
Controllers.renderAdminPage = (req, res) => {
res.render('admin/plugins/persona', {
title: 'Persona Theme',
});
};

View File

@ -1,30 +1,185 @@
'use strict'; 'use strict';
const Theme = module.exports; const nconf = require.main.require('nconf');
const meta = require.main.require('./src/meta');
const _ = require.main.require('lodash');
const user = require.main.require('./src/user');
const controllers = require('./controllers');
const library = module.exports;
const defaults = {
enableQuickReply: 'on',
enableBreadcrumbs: 'on',
centerHeaderElements: 'off',
mobileTopicTeasers: 'off',
stickyToolbar: 'on',
autohideBottombar: 'on',
openSidebars: 'off',
chatModals: 'off',
};
library.init = async function (params) {
const { router, middleware } = params;
const routeHelpers = require.main.require('./src/routes/helpers');
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/theme-quickstart', [], controllers.renderAdminPage);
routeHelpers.setupPageRoute(router, '/user/:userslug/theme', [
middleware.exposeUid,
middleware.ensureLoggedIn,
middleware.canViewUsers,
middleware.checkAccountPermissions,
], controllers.renderThemeSettings);
if (nconf.get('isPrimary') && process.env.NODE_ENV === 'production') {
setTimeout(buildSkins, 0);
}
};
async function buildSkins() {
try {
const plugins = require.main.require('./src/plugins');
await plugins.prepareForBuild(['client side styles']);
for (const skin of meta.css.supportedSkins) {
// eslint-disable-next-line no-await-in-loop
await meta.css.buildBundle(`client-${skin}`, true);
}
require.main.require('./src/meta/minifier').killAll();
} catch (err) {
console.error(err.stack);
}
}
library.addAdminNavigation = async function (header) {
header.plugins.push({
route: '/plugins/theme-quickstart',
icon: 'fa-paint-brush',
name: 'Theme Quick Start',
});
return header;
};
library.addProfileItem = async (data) => {
data.links.push({
id: 'theme',
route: 'theme',
icon: 'fa-paint-brush',
name: '[[themes/harmony:settings.title]]',
visibility: {
self: true,
other: false,
moderator: false,
globalMod: false,
admin: false,
},
});
return data;
};
library.defineWidgetAreas = async function (areas) {
const locations = ['header', 'sidebar', 'footer'];
const templates = [
'categories.tpl', 'category.tpl', 'topic.tpl', 'users.tpl',
'unread.tpl', 'recent.tpl', 'popular.tpl', 'top.tpl', 'tags.tpl', 'tag.tpl',
'login.tpl', 'register.tpl',
];
function capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
templates.forEach((template) => {
locations.forEach((location) => {
areas.push({
name: `${capitalizeFirst(template.split('.')[0])} ${capitalizeFirst(location)}`,
template: template,
location: location,
});
});
});
Theme.defineWidgetAreas = async function (areas) {
areas = areas.concat([ areas = areas.concat([
{ {
name: 'MOTD', name: 'Main post header',
template: 'home.tpl',
location: 'motd',
},
{
name: 'Homepage Footer',
template: 'home.tpl',
location: 'footer',
},
{
name: 'Category Sidebar',
template: 'category.tpl',
location: 'sidebar',
},
{
name: 'Topic Footer',
template: 'topic.tpl', template: 'topic.tpl',
location: 'footer', location: 'mainpost-header',
},
{
name: 'Main post footer',
template: 'topic.tpl',
location: 'mainpost-footer',
},
{
name: 'Sidebar Footer',
template: 'global',
location: 'sidebar-footer',
},
{
name: 'Brand Header',
template: 'global',
location: 'brand-header',
},
{
name: 'About me (before)',
template: 'account/profile.tpl',
location: 'profile-aboutme-before',
},
{
name: 'About me (after)',
template: 'account/profile.tpl',
location: 'profile-aboutme-after',
}, },
]); ]);
return areas; return areas;
}; };
library.loadThemeConfig = async function (uid) {
const [themeConfig, userConfig] = await Promise.all([
meta.settings.get('harmony'),
user.getSettings(uid),
]);
const config = { ...defaults, ...themeConfig, ...(_.pick(userConfig, Object.keys(defaults))) };
config.enableQuickReply = config.enableQuickReply === 'on';
config.enableBreadcrumbs = config.enableBreadcrumbs === 'on';
config.centerHeaderElements = config.centerHeaderElements === 'on';
config.mobileTopicTeasers = config.mobileTopicTeasers === 'on';
config.stickyToolbar = config.stickyToolbar === 'on';
config.autohideBottombar = config.autohideBottombar === 'on';
config.openSidebars = config.openSidebars === 'on';
config.chatModals = config.chatModals === 'on';
return config;
};
library.getThemeConfig = async function (config) {
config.theme = await library.loadThemeConfig(config.uid);
config.openDraftsOnPageLoad = false;
return config;
};
library.getAdminSettings = async function (hookData) {
if (hookData.plugin === 'harmony') {
hookData.values = {
...defaults,
...hookData.values,
};
}
return hookData;
};
library.saveUserSettings = async function (hookData) {
Object.keys(defaults).forEach((key) => {
if (hookData.data.hasOwnProperty(key)) {
hookData.settings[key] = hookData.data[key] || undefined;
}
});
return hookData;
};
library.filterMiddlewareRenderHeader = async function (hookData) {
hookData.templateData.bootswatchSkinOptions = await meta.css.getSkinSwitcherOptions(hookData.req.uid);
return hookData;
};

View File

@ -2,7 +2,7 @@
"name": "nodebb-theme-quickstart", "name": "nodebb-theme-quickstart",
"version": "0.1.0", "version": "0.1.0",
"description": "Enter a description here", "description": "Enter a description here",
"main": "theme.less", "main": "lib/theme.js",
"keywords": [], "keywords": [],
"license": "MIT", "license": "MIT",
"husky": { "husky": {
@ -18,7 +18,6 @@
] ]
}, },
"dependencies": { "dependencies": {
"bent": "^7.3.12"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "11.0.0", "@commitlint/cli": "11.0.0",

View File

@ -1,14 +1,26 @@
{ {
"id": "nodebb-theme-quickstart", "id": "nodebb-theme-quickstart",
"library": "./lib/theme.js",
"hooks": [ "hooks": [
{ "hook": "filter:widgets.getAreas", "method": "defineWidgetAreas" } { "hook": "static:app.load", "method": "init" },
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
{ "hook": "filter:widgets.getAreas", "method": "defineWidgetAreas" },
{ "hook": "filter:config.get", "method": "getThemeConfig" },
{ "hook": "filter:settings.get", "method": "getAdminSettings"},
{ "hook": "filter:user.saveSettings", "method": "saveUserSettings" },
{ "hook": "filter:user.profileMenu", "method": "addProfileItem" },
{ "hook": "filter:middleware.renderHeader", "method": "filterMiddlewareRenderHeader" }
], ],
"scripts": [ "scripts": [
"public/client.js", "public/client.js",
"../nodebb-theme-harmony/public/harmony.js" "../nodebb-theme-harmony/public/harmony.js"
], ],
"templates": "templates",
"modules": { "modules": {
"../admin/plugins/theme-quickstart.js": "../nodebb-theme-harmony/public/admin.js",
"../client/account/theme.js": "../nodebb-theme-harmony/public/settings.js"
},
"staticDirs": {
"inter": "node_modules/@fontsource/inter/files",
"poppins": "node_modules/@fontsource/poppins/files"
} }
} }

View File

@ -1 +1,79 @@
// only overrides to bs5 variables here // only overrides to bs5 variables here
// below are the default values from harmony theme overrides.scss file
// feel free to change these for your custom theme
// Harmony colours
$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;
$primary: $blue !default;
$secondary: $gray-600 !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-100 !default;
$dark: $gray-900 !default;
$body-color: $gray-800 !default;
$body-bg: $white !default;
$body-tertiary-bg: $gray-200 !default;
$text-muted: $gray-600 !default;
$border-color: $gray-200 !default;
$link-color: shade-color($blue, 20%) !default;
$btn-ghost-hover-color: mix($light, $dark, 90%);
$btn-ghost-active-color: lighten($btn-ghost-hover-color, 5%);
$btn-ghost-hover-color-dark: mix($dark, $light, 90%);
$btn-ghost-active-color-dark: lighten($btn-ghost-hover-color-dark, 5%);
:root {
--btn-ghost-hover-color: #{$btn-ghost-hover-color};
--btn-ghost-active-color: #{$btn-ghost-active-color};
}
[data-bs-theme="dark"] {
--btn-ghost-hover-color: #{$btn-ghost-hover-color-dark};
--btn-ghost-active-color: #{$btn-ghost-active-color-dark};
}
// 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;
$link-decoration: none;
$link-hover-decoration: underline;
// 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;
$small-font-size: 0.875rem !default;
$breadcrumb-divider: quote("");
$breadcrumb-divider-color: $gray-500 !default;
$breadcrumb-active-color: $body-color !default;
$breadcrumb-item-padding-x: 12px !default;
.form-control::placeholder, .bootstrap-tagsinput::placeholder {
color: $gray-500 !important;
}

292
templates/account/info.tpl Normal file
View File

@ -0,0 +1,292 @@
<!-- IMPORT partials/account/header.tpl -->
<!-- IF sessions.length -->
<div class="row mb-3">
<div class="col-12 col-md-12">
<h4>[[global:sessions]]</h4>
<ul class="list-group" component="user/sessions">
{{{each sessions}}}
<li class="list-group-item" data-uuid="{../uuid}">
<div class="float-end">
<!-- IF isSelfOrAdminOrGlobalModerator -->
<!-- IF !../current -->
<button class="btn btn-sm btn-outline-secondary" type="button" data-action="revokeSession">Revoke Session</button>
<!-- ENDIF !../current -->
<!-- ENDIF isSelfOrAdminOrGlobalModerator -->
{function.userAgentIcons}
<i class="fa fa-circle text-<!-- IF ../current -->success<!-- ELSE -->muted<!-- ENDIF ../current -->"></i>
</div>
{../browser} {../version} on {../platform}<br />
<small class="timeago text-muted" title="{../datetimeISO}"></small>
<ul>
<li><strong>[[global:ip-address]]</strong>: {../ip}</li>
</ul>
</li>
{{{end}}}
</ul>
</div>
</div>
<!-- ENDIF sessions.length -->
<div class="row">
<div class="col-sm-6">
<div class="card mb-3">
<h5 class="card-header">
[[global:recentips]]
</h5>
<div class="card-body">
<ul>
{{{each ips}}}
<li>{@value}</li>
{{{end}}}
</ul>
</div>
</div>
<div class="card mb-3">
<h5 class="card-header">
[[user:info.username-history]]
</h5>
<div class="card-body">
<ul class="list-unstyled mb-0">
{{{ each usernames }}}
<li class="d-flex justify-content-between mb-1">
<span class="text-sm">{./value}</span>
<div>
{{{ if ./byUid }}}
<a class="lh-1" href="{{{ if ./byUser.remoteId }}}{config.relative_path}/user/{./byUser.remoteId}{{{ else }}}#{{{ end }}}">
{buildAvatar(./byUser, "18px", true)}</a>
{{{ end }}}
<span class="timeago text-sm lh-1" title="{./timestampISO}"></span>
</div>
</li>
{{{ end }}}
</ul>
</div>
</div>
<div class="card mb-3">
<h5 class="card-header">
[[user:info.email-history]]
</h5>
<div class="card-body">
<ul class="list-unstyled mb-0">
{{{ each emails }}}
<li class="d-flex justify-content-between mb-1">
<span class="text-sm">{./value}</span>
<div>
{{{ if ./byUid }}}
<a class="lh-1" href="{{{ if ./byUser.remoteId }}}{config.relative_path}/user/{./byUser.remoteId}{{{ else }}}#{{{ end }}}">
{buildAvatar(./byUser, "18px", true)}</a>
{{{ end }}}
<span class="timeago text-sm lh-1" title="{./timestampISO}"></span>
</div>
</li>
{{{ end }}}
</ul>
</div>
</div>
<!-- IF isAdminOrGlobalModerator -->
<div class="card">
<h5 class="card-header">
[[user:info.moderation-note]]
</h5>
<div class="card-body">
<textarea component="account/moderation-note" class="form-control"></textarea>
<br/>
<button class="btn btn-sm float-end btn-success" component="account/save-moderation-note">[[user:info.moderation-note.add]]</button>
<br/>
<div component="account/moderation-note/list">
{{{ each moderationNotes }}}
<hr/>
<div data-id="{./id}">
<div class="mb-1">
<a href="{{{ if ./user.remoteId }}}{config.relative_path}/user/{./user.remoteId}{{{ else }}}#{{{ end }}}">{buildAvatar(./user, "24px", true)}</a>
<a href="{{{ if ./user.remoteId }}}{config.relative_path}/user/{./user.remoteId}{{{ else }}}#{{{ end }}}" class="fw-bold" itemprop="author" data-username="{./user.username}" data-uid="{./user.uid}">{./user.username}</a>
<span class="timeago" title="{./timestampISO}"></span>
</div>
<div component="account/moderation-note/content-area" class="d-flex flex-column">
<div class="content">
{./note}
</div>
<button component="account/moderation-note/edit" class="btn btn-sm btn-link align-self-end">[[topic:edit]]</button>
</div>
<div component="account/moderation-note/edit-area" class="d-flex flex-column gap-2">
<textarea class="form-control w-100 overflow-hidden">{./rawNote}</textarea>
<div class="align-self-end">
<button component="account/moderation-note/cancel-edit" class="btn btn-sm btn-link text-danger align-self-end">[[global:cancel]]</button>
<button component="account/moderation-note/save-edit" class="btn btn-sm btn-primary align-self-end">[[global:save]]</button>
</div>
</div>
</div>
{{{ end }}}
</div>
<!-- IMPORT partials/paginator.tpl -->
</div>
</div>
<!-- ENDIF isAdminOrGlobalModerator -->
</div>
<div class="col-sm-6">
<div class="card mb-3">
<h5 class="card-header">
[[user:info.latest-flags]]
</h5>
<div class="card-body">
<!-- IF history.flags.length -->
<ul class="recent-flags list-unstyled">
{{{ each history.flags }}}
<li class="mb-4 border-bottom">
<div class="mb-1 d-flex align-items-center justify-content-between">
<div>
{{{ if (./type == "user")}}}
<span class="badge text-bg-info">[[user:info.profile]]</span>
{{{ else }}}
<span class="badge text-bg-info">[[user:info.post]]</span>
{{{ end }}}
<span class="timestamp timeago" title="{./timestampISO}"></span>
</div>
<a href="{config.relative_path}/flags/{./flagId}" class="badge badge border border-gray-300 text-body">[[user:info.view-flag]]</a>
</div>
{{{ if (./type == "post") }}}
<p class="mb-1">
{{{ if history.flags.targetPurged }}}
<div>[[flags:target-purged]]</div>
{{{ else }}}
<a class="title" href="{config.relative_path}/post/{./pid}">{./title}</a>
{{{ end }}}
</p>
{{{ end }}}
<div class="d-flex gap-2 align-items-center mb-3">
<span class="text-sm">[[user:info.reported-by]]</span>
<div class="d-flex text-nowrap">
{{{ each ./reports }}}
<a style="width: 18px; z-index: 3;" class="text-decoration-none" href="{config.relative_path}/user/{./reporter.remoteId}">{buildAvatar(./reporter, "24px", true)}</a>
{{{ end }}}
</div>
</div>
</li>
{{{ end }}}
</ul>
<!-- ELSE -->
<div class="alert alert-success">[[user:info.no-flags]]</div>
<!-- ENDIF history.flags.length -->
</div>
</div>
<div class="card mb-3">
<h5 class="card-header">
[[user:info.ban-history]]
<!-- IF !banned -->
<!-- IF !isSelf -->
<button class="btn btn-sm float-end btn-danger" component="account/ban">[[user:ban-account]]</button>
<!-- ENDIF !isSelf -->
<!-- ELSE -->
<!-- IF !isSelf -->
<button class="btn btn-sm float-end btn-success" component="account/unban">[[user:unban-account]]</button>
<!-- ENDIF !isSelf -->
<!-- ENDIF !banned -->
</h5>
<div class="card-body">
<!-- IF history.bans.length -->
<ul class="ban-history list-unstyled">
{{{ each history.bans }}}
<li class="mb-4 border-bottom">
<div class="mb-1 d-flex align-items-center justify-content-between">
<div>
<a href="{config.relative_path}/user/{./user.remoteId}">{buildAvatar(./user, "24px", true)}</a>
<strong>
<a href="{{{ if ./user.remoteId }}}{config.relative_path}/user/{./user.remoteId}{{{ else }}}#{{{ end }}}" itemprop="author" data-username="{./user.username}" data-uid="{./user.uid}">{./user.username}</a>
</strong>
<span class="timestamp timeago" title="{./timestampISO}"></span>
</div>
{{{ if (./type != "unban") }}}
<span class="badge text-bg-danger">[[user:banned]]</span>
{{{ else }}}
<span class="badge text-bg-success">[[user:unbanned]]</span>
{{{ end }}}
</div>
<p class="mb-1">
<span class="reason">[[user:info.banned-reason-label]]: <strong>{./reason}</strong></span>
</p>
<p class="">
{{{ if ./until }}}
<span class="expiry">[[user:info.banned-until, {isoTimeToLocaleString(./untilISO, config.userLang)}]]</span>
{{{ else }}}
{{{ if (./type != "unban") }}}
<span class="expiry">[[user:info.banned-permanently]]</span>
{{{ end }}}
{{{ end }}}
</p>
</li>
{{{ end }}}
</ul>
<!-- ELSE -->
<div class="alert alert-success">[[user:info.no-ban-history]]</div>
<!-- ENDIF history.bans.length -->
</div>
</div>
<div class="card mb-3">
<h5 class="card-header">
[[user:info.mute-history]]
{{{ if !muted }}}
{{{ if !isSelf }}}
<button class="btn btn-sm float-end btn-danger" component="account/mute">[[user:mute-account]]</button>
{{{ end }}}
{{{ else }}}
{{{ if !isSelf }}}
<button class="btn btn-sm float-end btn-success" component="account/unmute">[[user:unmute-account]]</button>
{{{ end }}}
{{{ end }}}
</h5>
<div class="card-body">
{{{ if history.mutes.length }}}
<ul class="ban-history list-unstyled">
{{{ each history.mutes }}}
<li class="mb-4 border-bottom">
<div class="mb-1 d-flex align-items-center justify-content-between">
<div>
<a href="{config.relative_path}/user/{./user.remoteId}">{buildAvatar(./user, "24px", true)}</a>
<strong>
<a href="{{{ if ./user.remoteId }}}{config.relative_path}/user/{./user.remoteId}{{{ else }}}#{{{ end }}}" itemprop="author" data-username="{./user.username}" data-uid="{./user.uid}">{./user.username}</a>
</strong>
<span class="timestamp timeago" title="{./timestampISO}"></span>
</div>
{{{ if (./type != "unmute") }}}
<span class="badge text-bg-danger">[[user:muted]]</span>
{{{ else }}}
<span class="badge text-bg-success">[[user:unmuted]]</span>
{{{ end }}}
</div>
<p class="mb-1">
<span class="reason">[[user:info.banned-reason-label]]: <strong>{./reason}</strong></span>
</p>
<p class="">
{{{ if ./until }}}
<span class="expiry">[[user:info.muted-until, {isoTimeToLocaleString(./untilISO, config.userLang)}]]</span>
{{{ end }}}
</p>
</li>
{{{ end }}}
</ul>
{{{ else }}}
<div class="alert alert-success">[[user:info.no-mute-history]]</div>
{{{ end }}}
</div>
</div>
</div>
</div>
<!-- IMPORT partials/account/footer.tpl -->

View File

@ -0,0 +1,24 @@
<div class="acp-page-container">
<!-- IMPORT admin/partials/settings/header.tpl -->
<div class="row m-0">
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
<form role="form" class="persona-settings">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="hideSubCategories" name="hideSubCategories">
<label class="form-check-label">Hide subcategories on categories view</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="hideCategoryLastPost" name="hideCategoryLastPost">
<label class="form-check-label">Hide last post on categories view</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enableQuickReply" name="enableQuickReply">
<label class="form-check-label">Enable quick reply</label>
</div>
</form>
</div>
<!-- IMPORT admin/partials/settings/toc.tpl -->
</div>
</div>

View File

@ -0,0 +1,56 @@
<div class="acp-page-container">
<!-- IMPORT admin/partials/settings/header.tpl -->
<div class="row m-0">
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
<form role="form" class="harmony-settings">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="enableQuickReply" name="enableQuickReply" />
<label for="enableQuickReply" class="form-check-label">[[themes/harmony:settings.enableQuickReply]]</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="enableBreadcrumbs" name="enableBreadcrumbs" />
<label for="enableBreadcrumbs" class="form-check-label">[[themes/harmony:settings.enableBreadcrumbs]]</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="centerHeaderElements" name="centerHeaderElements" />
<label for="centerHeaderElements" class="form-check-label">[[themes/harmony:settings.centerHeaderElements]]</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="mobileTopicTeasers" name="mobileTopicTeasers" />
<label for="mobileTopicTeasers" class="form-check-label">[[themes/harmony:settings.mobileTopicTeasers]]</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="stickyToolbar" name="stickyToolbar" />
<div for="stickyToolbar" class="form-check-label">
[[themes/harmony:settings.stickyToolbar]]
<p class="form-text">
[[themes/harmony:settings.stickyToolbar.help]]
</p>
</div>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="autohideBottombar" name="autohideBottombar" />
<div for="autohideBottombar" class="form-check-label">
[[themes/harmony:settings.autohideBottombar]]
<p class="form-text">
[[themes/harmony:settings.autohideBottombar.help]]
</p>
</div>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="openSidebars" name="openSidebars" />
<label for="openSidebars" class="form-check-label">[[themes/harmony:settings.openSidebars]]</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="chatModals" name="chatModals" />
<div for="chatModals" class="form-check-label">
[[themes/harmony:settings.chatModals]]
</div>
</div>
</form>
</div>
<!-- IMPORT admin/partials/settings/toc.tpl -->
</div>
</div>

130
templates/header.tpl Normal file
View File

@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="{function.localeToHTML, userLang, defaultLang}" {{{if languageDirection}}}data-dir="{languageDirection}" style="direction: {languageDirection};"{{{end}}}>
<head>
<title>{browserTitle}</title>
{{{each metaTags}}}{function.buildMetaTag}{{{end}}}
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/client{{{if bootswatchSkin}}}-{bootswatchSkin}{{{end}}}{{{ if (languageDirection=="rtl") }}}-rtl{{{ end }}}.css?{config.cache-buster}" />
{{{each linkTags}}}{function.buildLinkTag}{{{end}}}
<script>
var config = JSON.parse('{{configJSON}}');
var app = {
user: JSON.parse('{{userJSON}}')
};
document.documentElement.style.setProperty('--panel-offset', `${localStorage.getItem('panelOffset') || 0}px`);
</script>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script>
var config = JSON.parse('{{configJSON}}');
var app = {
user: JSON.parse('{{userJSON}}')
};
fetch('/frames/totals').then((res) => {
return res.json()
}).then((obj) => {
document.getElementById("sss").innerText = obj.stories;
document.getElementById("aaa").innerText = obj.authors;
});
$(document).ready(function() {
$(".navbar-burger").click(function() {
$(".navbar-burger").toggleClass("is-active");
$(".navbar-menu").toggleClass("is-active");
})
$('.has-dropdown > .navbar-link').click(function() {
$('.navbar-dropdown').toggle()
})
})
$(document).mouseup(function(e) {
var container = $(".navbar-menu, .navbar-burger, .navbar-burger > span");
if (!container.is(e.target) && container.has(e.target).length === 0) {
$(".navbar-burger").removeClass("is-active");
$(".navbar-menu").removeClass("is-active");
}
});
document.documentElement.style.setProperty('--panel-offset', `${localStorage.getItem('panelOffset') || 0}px`);
</script>
{{{if useCustomHTML}}}
{{customHTML}}
{{{end}}}
{{{if useCustomCSS}}}
<style>{{customCSS}}</style>
{{{end}}}
</head>
<body class="{bodyClass} skin-{{{if bootswatchSkin}}}{bootswatchSkin}{{{else}}}noskin{{{end}}}">
<nav id="menu" class="slideout-menu hidden">
<!-- IMPORT partials/slideout-menu.tpl -->
</nav>
<nav id="chats-menu" class="slideout-menu hidden">
<!-- IMPORT partials/chats-menu.tpl -->
</nav>
<main id="panel" class="slideout-panel">
<div id="navstufz">
<nav class="navbar container is-fluid pb-4">
<div class="sitetitle navbar-brand">Rockfic</div>
<div id="sitesubtitle" class="is-flex">
<div class="subtitle is-5 m-0"> — Band fiction that rocks</div>
<div class="content is-small">With <div id="sss" style="display: inline-block">0</div> stories by <div id="aaa" style="display: inline-block"></div> authors</div>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="/">Home</a>
<a class="navbar-item" href="/bands">Bands</a>
<a class="navbar-item" href="/authors">Authors</a>
<a class="navbar-item" href="/forum">Message Board</a>
<!-- IF config.loggedIn -->
{{{ if config.loggedIn }}}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">Your Stuff</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="/my-stuff">Account</a>
<a class="navbar-item" href="/messages">
Private Messages
</a>
<a class="navbar-item" href="/my-stuff/reviews">Manage Reviews</a>
<a class="navbar-item" href="/my-stuff/profile">Edit Profile</a>
<a class="navbar-item" href="/user/{user.remoteId}">View Profile</a>
<a class="navbar-item" href="/my-stuff/stories">Stories</a>
<a class="navbar-item" href="/my-stuff/drafts">Drafts</a>
<!--IF isAdmin -->
{{{if isAdmin}}}
<hr class="navbar-divider">
<a class="navbar-item" href="/admin">Admin page</a>
{{{end}}}
<!-- ENDIF isAdmin-->
</div>
</div>
<a class="navbar-item" href="/logout">Log Out</a>
{{{else}}}
<a class="navbar-item button is-light mr-3" href="/login">Log In</a>
<a class="navbar-item button is-primary" href="/register">Register!</a>
{{{end}}}
</div>
<div id="addlink" class="navbar-end" style="align-items: center !important">
<a class="button is-primary" href="/new-story">Submit a New Story!</a>
</div>
</div>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</nav>
</div>
<nav class="navbar sticky-top navbar-expand-lg bg-light header border-bottom py-0" id="header-menu" component="navbar">
<div class="container-lg justify-content-start flex-nowrap">
<!-- IMPORT partials/menu.tpl -->
</div>
</nav>
<script>
const rect = document.getElementById('header-menu').getBoundingClientRect();
const offset = Math.max(0, rect.bottom);
document.documentElement.style.setProperty('--panel-offset', offset + `px`);
</script>
<div class="container-lg pt-3" id="content" style="margin: 0 auto;" >
<!-- IMPORT partials/noscript/warning.tpl -->
<!-- IMPORT partials/noscript/message.tpl -->

View File

@ -0,0 +1,24 @@
<div class="lastpost border-start border-4 lh-sm h-100" style="border-color: {./bgColor}!important;">
{{{ each ./posts }}}
{{{ if @first }}}
<div component="category/posts" class="ps-2 text-xs d-flex flex-column h-100 gap-1">
<div class="text-nowrap text-truncate">
<a class="text-decoration-none avatar-tooltip" title="{./user.username}" href="{config.relative_path}/user/{./user.remoteId}">{buildAvatar(posts.user, "18px", true)}</a>
<a class="permalink text-muted timeago text-xs" href="{config.relative_path}/topic/{./topic.slug}{{{ if ./index }}}/{./index}{{{ end }}}" title="{./timestampISO}" aria-label="[[global:lastpost]]"></a>
</div>
<div class="post-content text-xs text-break line-clamp-sm-2 lh-sm position-relative flex-fill">
<a class="stretched-link" tabindex="-1" href="{config.relative_path}/topic/{./topic.slug}{{{ if ./index }}}/{./index}{{{ end }}}" aria-label="[[global:lastpost]]"></a>
{./content}
</div>
</div>
{{{ end }}}
{{{ end }}}
{{{ if !./posts.length }}}
<div component="category/posts" class="ps-2">
<div class="post-content overflow-hidden text-xs">
[[category:no-new-posts]]
</div>
</div>
{{{ end }}}
</div>

View File

@ -0,0 +1,41 @@
{{{ if config.loggedIn }}}
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link text-decoration-none" href="#" data-bs-target="#notifications" data-bs-toggle="tab"><span class="counter unread-count" component="notifications/icon" data-content="{unreadCount.notification}"></span> <i class="fa fa-fw fa-bell"></i></a>
</li>
{{{ if !config.disableChat }}}
<li class="nav-item">
<a class="nav-link text-decoration-none" href="#" data-bs-target="#chats" data-bs-toggle="tab"><i class="counter unread-count" component="chat/icon" data-content="{unreadCount.chat}"></i> <i class="fa fa-fw fa-comment"></i></a>
</li>
{{{ end }}}
<li class="nav-item">
<a class="nav-link active text-decoration-none" href="#" data-bs-target="#profile" data-bs-toggle="tab">
{buildAvatar(user, "24px", true, "user-icon")}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="profile">
<section class="menu-section" data-section="profile">
<ul class="menu-section-list dropdown-menu show text-bg-dark w-100 border-0" component="header/usercontrol"></ul>
</section>
</div>
<div class="tab-pane fade" id="notifications">
<section class="menu-section text-bg-dark px-1" data-section="notifications">
<ul class="menu-section-list notification-list-mobile list-unstyled" component="notifications/list"></ul>
<div class="menu-section-list text-center p-3"><a href="{relative_path}/notifications">[[notifications:see-all]]</a></div>
</section>
</div>
{{{ if !config.disableChat }}}
<div class="tab-pane fade" id="chats">
<section class="menu-section text-bg-dark px-1" data-section="chats">
<ul class="menu-section-list chat-list list-unstyled" component="chat/list">
</ul>
<div class="menu-section-list text-center p-3"><a class="navigation-link" href="{relative_path}/user/{user.remoteId}/chats">[[modules:chat.see-all]]</a></div>
</section>
</div>
{{{ end }}}
</div>
{{{ end }}}

View File

@ -0,0 +1,33 @@
<label class="text-xs text-muted">[[groups:invited.search]]</label>
<div class="input-group mb-2">
<input class="form-control" type="text" component="groups/members/invite"/>
<span class="input-group-text search-button"><i class="fa fa-search"></i></span>
</div>
<div class="mb-2">
<label class="text-xs text-muted">[[groups:bulk-invite-instructions]]</label>
<textarea class="form-control" component="groups/members/bulk-invite"></textarea>
</div>
<div class="mb-2 clearfix">
<button type="button" class="btn btn-primary btn-sm float-end" component="groups/members/bulk-invite-button">[[groups:bulk-invite]]</button>
</div>
<div style="max-height: 500px; overflow: auto;">
<div component="groups/invited/alert" class="alert alert-info {{{ if group.invited.length }}}hidden{{{ end }}}">[[groups:invited.none]]</div>
<table component="groups/invited" class="table table-hover">
<tbody>
{{{ each group.invited }}}
<tr data-uid="{group.invited.uid}" class="align-middle">
<td class="member-name p-2 d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<a class="text-decoration-none" href="{config.relative_path}/user/{group.invited.remoteId}">{buildAvatar(group.invited, "24px", true)}</a>
<a href="{config.relative_path}/user/{group.invited.remoteId}">{group.invited.username}</a>
</div>
<button class="btn btn-outline-secondary btn-sm text-nowrap" data-action="rescindInvite">[[groups:invited.uninvite]]</button>
</td>
</tr>
{{{ end }}}
</tbody>
</table>
</div>

View File

@ -0,0 +1,21 @@
{{{each groups}}}
<div class="col-lg-4 col-md-6 col-sm-12 mb-3" component="groups/summary" data-slug="{groups.slug}">
<div class="card h-100">
<a href="{config.relative_path}/groups/{groups.slug}" class="card-header list-cover" style="{{{ if groups.cover:thumb:url }}}background-image: url({./cover:thumb:url});background-size: cover; min-height: 125px; background-position: {./cover:position}{{{ end }}}">
<h5 class="card-title d-inline-block mw-100 px-2 py-1 text-truncate text-capitalize fw-bold rounded-1" style="color: white;background-color: rgba(0,0,0,0.5);">{groups.displayName} <small>{formattedNumber(groups.memberCount)}</small></h5>
</a>
<div class="card-body">
<p class="text-muted">
{./description}
</p>
<ul class="members list-unstyled d-flex align-items-center gap-2 flex-wrap">
{{{each groups.members}}}
<li>
<a href="{config.relative_path}/user/{groups.members.remoteId}">{buildAvatar(groups.members, "24px", true)}</a>
</li>
{{{end}}}
</ul>
</div>
</div>
</div>
{{{end}}}

View File

@ -0,0 +1,43 @@
<div class="d-flex mb-3">
<!-- IF group.isOwner -->
<div class="flex-shrink-0">
<button component="groups/members/add" type="button" class="btn btn-primary me-3" title="[[groups:details.add-member]]"><i class="fa fa-user-plus"></i></button>
</div>
<!-- ENDIF group.isOwner -->
<div class="flex-grow-1">
<div class="input-group">
<input class="form-control" type="text" component="groups/members/search" placeholder="[[global:search]]"/>
<span class="input-group-text search-button"><i class="fa fa-search"></i></span>
</div>
</div>
</div>
<div component="groups/members" data-nextstart="{group.membersNextStart}" style="max-height: 500px; overflow: auto;">
<table class="table table-striped table-hover">
<tbody>
{{{each group.members}}}
<tr data-uid="{group.members.uid}" data-isowner="{{{ if group.members.isOwner }}}1{{{ else }}}0{{{ end }}}">
<td class="p-2">
<a href="{config.relative_path}/user/{group.members.remoteId}">{buildAvatar(group.members, "24px", true)}</a>
</td>
<td class="member-name w-100 p-2">
<a class="align-text-top" href="{config.relative_path}/user/{group.members.remoteId}">{group.members.username}</a>
<i component="groups/owner/icon" title="[[groups:owner]]" class="user-owner-icon fa fa-star align-text-top text-warning <!-- IF !group.members.isOwner -->invisible<!-- ENDIF !group.members.isOwner -->"></i>
<!-- IF group.isOwner -->
<div class="owner-controls btn-group float-end">
<a class="btn btn-sm" href="#" data-ajaxify="false" data-action="toggleOwnership" title="[[groups:details.grant]]">
<i class="fa fa-star"></i>
</a>
<a class="btn btn-sm" href="#" data-ajaxify="false" data-action="kick" title="[[groups:details.kick]]">
<i class="fa fa-ban"></i>
</a>
</div>
<!-- ENDIF group.isOwner -->
</td>
</tr>
{{{end}}}
</tbody>
</table>
</div>

View File

@ -0,0 +1,28 @@
{{{ if group.pending.length }}}
<div class="d-flex justify-content-end gap-2 mb-3">
<button class="btn btn-danger btn-sm" data-action="rejectAll">[[groups:pending.reject-all]]</button>
<button class="btn btn-success btn-sm" data-action="acceptAll">[[groups:pending.accept-all]]</button>
</div>
{{{ end }}}
<div style="max-height: 500px;overflow: auto;">
<div component="groups/pending/alert" class="alert alert-info {{{ if group.pending.length }}}hidden{{{ end }}}">[[groups:pending.none]]</div>
<table component="groups/pending" class="table table-hover">
<tbody>
{{{ each group.pending }}}
<tr data-uid="{group.pending.uid}" class="align-middle">
<td class="member-name p-2 d-flex align-items-center justify-content-between">
<div class="d-flex gap-2">
<a class="text-decoration-none" href="{config.relative_path}/user/{group.pending.remoteId}">{buildAvatar(group.pending, "24px", true)}</a>
<a href="{config.relative_path}/user/{group.pending.remoteId}">{group.pending.username}</a>
</div>
<div class="d-flex gap-2">
<button class="btn btn-danger btn-sm" data-action="reject">[[groups:pending.reject]]</a></li>
<button class="btn btn-success btn-sm" data-action="accept">[[groups:pending.accept]]</a></li>
</div>
</td>
</tr>
{{{ end }}}
</tbody>
</table>
</div>

View File

@ -0,0 +1,35 @@
<a class="nav-link" data-bs-toggle="dropdown" href="{relative_path}/user/{user.remoteId}/chats" data-ajaxify="false" id="chat_dropdown" component="chat/dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<i component="chat/icon" class="fa {{{ if unreadCount.chat}}}fa-comment{{{ else }}}fa-comment-o{{{ end }}} fa-fw unread-count" data-content="{unreadCount.chat}"></i> <span class="d-inline d-sm-none">[[global:header.chats]]</span>
</a>
<ul class="dropdown-menu dropdown-menu-end p-1" aria-labelledby="chat_dropdown" role="menu">
<li>
<ul component="chat/list" class="list-unstyled chat-list chats-list ghost-scrollbar pe-1">
<div class="rounded-1">
<div class="d-flex gap-1 justify-content-between">
<div class="dropdown-item p-2 d-flex gap-2 placeholder-wave">
<div class="main-avatar">
<div class="placeholder" style="width: 32px; height: 32px;"></div>
</div>
<div class="d-flex flex-grow-1 flex-column w-100">
<div class="text-xs"><div class="placeholder col-3"></div></div>
<div class="text-sm"><div class="placeholder col-11"></div></div>
<div class="text-xs"><div class="placeholder col-4"></div></div>
</div>
</div>
<div>
<button class="mark-read btn btn-ghost btn-sm d-flex align-items-center justify-content-center flex-grow-0 flex-shrink-0 p-1" style="width: 1.5rem; height: 1.5rem;">
<i class="unread fa fa-2xs fa-circle text-primary"></i>
</button>
</div>
</div>
</div>
</ul>
</li>
<li class="dropdown-divider"></li>
<li>
<div class="d-flex justify-content-center gap-1 flex-wrap">
<a class="btn btn-light btn-sm mark-all-read flex-fill text-nowrap" href="#" component="chats/mark-all-read"><i class="fa fa-check-double"></i> [[modules:chat.mark-all-read]]</a>
<a class="btn btn-primary btn-sm flex-fill text-nowrap" href="{relative_path}/user/{user.remoteId}/chats"><i class="fa fa-comments"></i> [[modules:chat.see-all]]</a>
</div>
</li>
</ul>

View File

@ -0,0 +1,102 @@
<li id="user_label" class="nav-item dropdown px-3" title="[[global:header.profile]]">
<a href="#" for="user-control-list-check" data-bs-toggle="dropdown" id="user_dropdown" role="button" component="header/avatar" aria-haspopup="true" aria-expanded="false">
{buildAvatar(user, "32px", true)}
<span id="user-header-name" class="d-block d-sm-none">{user.username}</span>
</a>
<input type="checkbox" class="hidden" id="user-control-list-check" aria-hidden="true">
<ul id="user-control-list" component="header/usercontrol" class="overscroll-behavior-contain user-dropdown dropdown-menu dropdown-menu-end shadow p-1 text-sm ff-base" role="menu">
<li>
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" component="header/profilelink" href="{relative_path}/user/{user.remoteId}" role="menuitem" aria-label="[[user:profile]]">
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status {user.status}"><span class="visually-hidden">[[global:{user.status}]]</span></span>
<span class="fw-semibold" component="header/username">{user.username}</span>
</a>
</li>
<li role="presentation" class="dropdown-divider"></li>
<li><h6 class="dropdown-header text-xs">[[global:status]]</h6></li>
<li>
<a href="#" class="dropdown-item rounded-1 user-status d-flex align-items-center gap-2 {{{ if user.online }}}selected{{{ end }}}" data-status="online" role="menuitem">
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status online"></span>
<span class="flex-grow-1">[[global:online]]</span>
</a>
</li>
<li>
<a href="#" class="dropdown-item rounded-1 user-status d-flex align-items-center gap-2 {{{ if user.away }}}selected{{{ end }}}" data-status="away" role="menuitem">
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status away"></span>
<span class="flex-grow-1">[[global:away]]</span>
</a>
</li>
<li>
<a href="#" class="dropdown-item rounded-1 user-status d-flex align-items-center gap-2 {{{ if user.dnd }}}selected{{{ end }}}" data-status="dnd" role="menuitem">
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status dnd"></span>
<span class="flex-grow-1">[[global:dnd]]</span>
</a>
</li>
<li>
<a href="#" class="dropdown-item rounded-1 user-status d-flex align-items-center gap-2 {{{ if user.offline }}}selected{{{ end }}}" data-status="offline" role="menuitem">
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status offline"></span>
<span class="flex-grow-1">[[global:invisible]]</span>
</a>
</li>
<li role="presentation" class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="{relative_path}/user/{user.remoteId}/bookmarks" role="menuitem">
<i class="fa fa-fw fa-bookmark"></i> <span>[[user:bookmarks]]</span>
</a>
</li>
<li>
<a class="dropdown-item" component="header/profilelink/edit" href="{relative_path}/user/{user.remoteId}/edit" role="menuitem">
<i class="fa fa-fw fa-edit"></i> <span>[[user:edit-profile]]</span>
</a>
</li>
<li>
<a class="dropdown-item" component="header/profilelink/settings" href="{relative_path}/user/{user.remoteId}/settings" role="menuitem">
<i class="fa fa-fw fa-gear"></i> <span>[[user:settings]]</span>
</a>
</li>
{{{ if showModMenu }}}
<li role="presentation" class="dropdown-divider"></li>
<li><h6 class="dropdown-header">[[pages:moderator-tools]]</h6></li>
<li>
<a class="dropdown-item" href="{relative_path}/flags" role="menuitem">
<i class="fa fa-fw fa-flag"></i> <span>[[pages:flagged-content]]</span>
</a>
</li>
<li>
<a class="dropdown-item" href="{relative_path}/post-queue" role="menuitem">
<i class="fa fa-fw fa-list-alt"></i> <span>[[pages:post-queue]]</span>
</a>
</li>
{{{ if registrationQueueEnabled }}}
<li>
<a class="dropdown-item" href="{relative_path}/registration-queue" role="menuitem">
<i class="fa fa-fw fa-list-alt"></i> <span>[[pages:registration-queue]]</span>
</a>
</li>
{{{ end }}}
<li>
<a class="dropdown-item" href="{relative_path}/ip-blacklist" role="menuitem">
<i class="fa fa-fw fa-ban"></i> <span>[[pages:ip-blacklist]]</span>
</a>
</li>
{{{ else }}}
{{{ if postQueueEnabled }}}
<li>
<a class="dropdown-item" href="{relative_path}/post-queue" role="menuitem">
<i class="fa fa-fw fa-list-alt"></i> <span>[[pages:post-queue]]</span>
</a>
</li>
{{{ end }}}
{{{ end }}}
<li role="presentation" class="dropdown-divider"></li>
<li component="user/logout">
<form method="post" action="{relative_path}/logout">
<input type="hidden" name="_csrf" value="{config.csrf_token}">
<input type="hidden" name="noscript" value="true">
<button type="submit" class="dropdown-item" role="menuitem">
<i class="fa fa-fw fa-sign-out"></i><span> [[global:logout]]</span>
</button>
</form>
</li>
</ul>
</li>

View File

@ -0,0 +1,44 @@
{{{ if !notifications.length }}}
<div class="no-notifs text-center p-4 d-flex flex-column">
<div class="p-4"><i class="fa-solid fa-wind fs-2 text-muted"></i></div>
<div class="text-xs fw-semibold text-muted">[[notifications:no-notifs]]</div>
</div>
{{{ end }}}
{{{ each notifications }}}
<div class="{./readClass}" data-nid="{./nid}" data-path="{./path}" {{{ if ./pid }}}data-pid="{./pid}"{{{ end }}}{{{ if ./tid }}}data-tid="{./tid}"{{{ end }}}>
<div class="d-flex gap-1 justify-content-between">
<div class="btn btn-ghost btn-sm d-flex gap-2 flex-grow-1 align-items-start text-start">
<a class="flex-grow-0 flex-shrink-0" href="{{{ if ./user.remoteId}}}{config.relative_path}/user/{./user.remoteId}{{{ else }}}#{{{ end }}}">
{{{ if (./image && ./from) }}}
<img class="avatar avatar-rounded" style="--avatar-size: 32px;" src="{./image}" />
{{{ else }}}
{{{ if ./icon }}}
<div class="avatar avatar-rounded" style="--avatar-size: 32px;"><i class="text-secondary fa {./icon}"></i></div>
{{{ else }}}
<div class="avatar avatar-rounded" style="--avatar-size: 32px; background-color: {./user.icon:bgColor};">{./user.icon:text}</div>
{{{ end }}}
{{{ end }}}
</a>
<div class="d-flex flex-grow-1 flex-column align-items-start position-relative">
<a href="{./path}" class="text-decoration-none d-inline-block text-reset text-break text-sm ff-sans stretched-link" component="notifications/item/link">
{./bodyShort}
</a>
<div class="text-xs text-muted">{{{ if ./timeagoLong }}}{./timeagoLong}{{{ else }}}<span class="timeago" title="{./datetimeISO}"></span>{{{ end }}}</div>
</div>
</div>
<div>
{{{ if ./nid }}}
<button class="mark-read btn btn-ghost btn-sm flex-grow-0 flex-shrink-0 p-1" style="width: 1.5rem; height: 1.5rem;">
<i class="unread fa fa-2xs fa-circle text-primary {{{ if ./read }}}hidden{{{ end }}}" aria-label="[[unread:mark-as-read]]"></i>
<i class="read fa fa-2xs fa-circle-o text-secondary {{{ if !./read }}}hidden{{{ end }}}" aria-label="[[unread:mark-as-unread]]"></i>
</button>
{{{ end }}}
</div>
</div>
</div>
{{{ if !@last }}}
<hr class="my-1" />
{{{ end }}}
{{{ end }}}

View File

@ -0,0 +1,34 @@
<li component="post" class="posts-list-item row {{{ if ./deleted }}} deleted{{{ else }}}{{{ if ./topic.deleted }}} deleted{{{ end }}}{{{ end }}}{{{ if ./topic.scheduled }}} scheduled{{{ end }}}" data-pid="{./pid}" data-uid="{./uid}">
<div class="col-lg-11 col-sm-10 col-9 post-body pb-3">
<a class="topic-title text-reset" href="{config.relative_path}/post/{encodeURIComponent(../pid)}">
{{{ if !./isMainPost }}}RE: {{{ end }}}{./topic.title}
</a>
<div component="post/content" class="content mb-3">
{../content}
</div>
<div class="mb-3">
<a class="topic-category text-xs fw-bold text-uppercase text-secondary mb-3" href="{config.relative_path}/category/{../category.slug}">[[global:posted-in, {../category.name}]]</a>
{{{ if ../isMainPost }}}
{{{ if ../topic.tags.length }}}
<span class="tag-list">
{{{ each ../topic.tags }}}
<a href="{config.relative_path}/tags/{topic.tags.valueEncoded}"><span class="tag tag-item tag-class-{topic.tags.class}">{topic.tags.valueEscaped}</span></a>
{{{ end }}}
</span>
{{{ end }}}
{{{ end }}}
</div>
<div class="post-info">
<a href="{config.relative_path}/user/{./user.remoteId}">{buildAvatar(./user, "28px", true, "user-img not-responsive")}</a>
<div class="post-author text-secondary text-uppercase">
<a class="text-reset" href="{config.relative_path}/user/{./user.remoteId}">{./user.displayname}</a><br />
<span class="timeago" title="{./timestampISO}"></span>
</div>
</div>
</div>
</li>

View File

@ -0,0 +1,54 @@
<div id="results" class="search-results col-md-12" data-search-query="{search_query}">
{{{ if matchCount }}}
<div class="alert alert-info">[[search:results-matching, {matchCount}, {search_query}, {time}]] </div>
{{{ else }}}
{{{ if search_query }}}
<div class="alert alert-warning">[[search:no-matches]]</div>
{{{ end }}}
{{{ end }}}
{{{each posts}}}
<div class="topic-row card clearfix mb-3">
<div class="card-body">
<div class="mb-2">
<a href="{config.relative_path}/user/{./user.remoteId}">{buildAvatar(./user, "24px", true)}</a>
<a class="topic-title fw-semibold fs-5" href="{config.relative_path}/post/{encodeURIComponent(posts.pid)}">{./topic.title}</a>
</div>
{{{ if showAsPosts }}}
<div component="post/content" class="content">
{./content}
</div>
{{{ end }}}
<small class="post-info">
<a href="{config.relative_path}/category/{./category.slug}">
<div class="category-item d-inline-block">
{buildCategoryIcon(./category, "24px", "rounded-circle")}
{./category.name}
</div>
</a> &bull;
<span class="timeago" title="{./timestampISO}"></span>
</small>
</div>
</div>
{{{end}}}
{{{ if users.length }}}
<!-- IMPORT partials/users_list.tpl -->
{{{ end }}}
{{{ if tags.length }}}
<!-- IMPORT partials/tags_list.tpl -->
{{{ end }}}
{{{ if categories.length }}}
<ul class="categories">
{{{each categories}}}
<!-- IMPORT partials/categories/item.tpl -->
{{{end}}}
</ul>
{{{ end }}}
<!-- IMPORT partials/paginator.tpl -->
</div>

View File

@ -0,0 +1,12 @@
<div class="clearfix">
<div class="icon float-start">
<a href="<!-- IF post.user.remoteId -->{config.relative_path}/user/{post.user.remoteId}<!-- ELSE -->#<!-- ENDIF post.user.remoteId -->">
{buildAvatar(post.user, "24px", true, "", "user/picture")} {post.user.username}
</a>
</div>
<small class="float-end">
<span class="timeago" title="{post.timestampISO}"></span>
</small>
</div>
<div>{post.content}</div>

View File

@ -0,0 +1 @@
<small data-editor="{editor.remoteId}" component="post/editor" class="hidden">[[global:last-edited-by, {editor.username}]] <span class="timeago" title="{isoTimeToLocaleString(editedISO, config.userLang)}"></span></small>

View File

@ -0,0 +1,140 @@
{{{ if (!./index && widgets.mainpost-header.length) }}}
<div data-widget-area="mainpost-header">
{{{ each widgets.mainpost-header }}}
{widgets.mainpost-header.html}
{{{ end }}}
</div>
{{{ end }}}
<div class="clearfix post-header">
<div class="icon float-start">
<a href="<!-- IF posts.user.remoteId -->{config.relative_path}/user/{posts.user.remoteId}<!-- ELSE -->#<!-- ENDIF posts.user.remoteId -->">
{buildAvatar(posts.user, "48px", true, "", "user/picture")}
{{{ if ./user.isLocal }}}
<span component="user/status" class="position-absolute top-100 start-100 border border-white border-2 rounded-circle status {posts.user.status}"><span class="visually-hidden">[[global:{posts.user.status}]]</span></span>
{{{ else }}}
<span component="user/locality" class="position-absolute top-100 start-100 lh-1 border border-white border-2 rounded-circle small" title="[[global:remote-user]]">
<span class="visually-hidden">[[global:remote-user]]</span>
<i class="fa fa-globe"></i>
</span>
{{{ end }}}
</a>
</div>
<small class="d-flex">
<div class="d-flex align-items-center gap-1 flex-wrap w-100">
<strong class="text-nowrap" itemprop="author" itemscope itemtype="https://schema.org/Person">
<meta itemprop="name" content="{./user.username}">
{{{ if ./user.remoteId }}}<meta itemprop="url" content="{config.relative_path}/user/{./user.remoteId}">{{{ end }}}
<a href="<!-- IF posts.user.remoteId -->{config.relative_path}/user/{posts.user.remoteId}<!-- ELSE -->#<!-- ENDIF posts.user.remoteId -->" data-username="{posts.user.username}" data-uid="{posts.user.uid}">{posts.user.displayname}</a>
</strong>
{{{ each posts.user.selectedGroups }}}
{{{ if posts.user.selectedGroups.slug }}}
<!-- IMPORT partials/groups/badge.tpl -->
{{{ end }}}
{{{ end }}}
<!-- IF posts.user.banned -->
<span class="badge bg-danger">[[user:banned]]</span>
<!-- ENDIF posts.user.banned -->
<span class="visible-xs-inline-block visible-sm-inline-block visible-md-inline-block visible-lg-inline-block">
{{{ if posts.toPid }}}
<a component="post/parent" class="btn btn-sm btn-ghost py-0 px-1 text-xs hidden-xs" data-topid="{posts.toPid}" href="{config.relative_path}/post/{posts.toPid}"><i class="fa fa-reply"></i> @{{{ if posts.parent.user.remoteId }}}{posts.parent.user.username}{{{ else }}}[[global:guest]]{{{ end }}}</a>
{{{ end }}}
<span>
<!-- IF posts.user.custom_profile_info.length -->
&#124;
{{{each posts.user.custom_profile_info}}}
{posts.user.custom_profile_info.content}
{{{end}}}
<!-- ENDIF posts.user.custom_profile_info.length -->
</span>
</span>
<div class="d-flex align-items-center gap-1 flex-grow-1 justify-content-end">
<span>
<i component="post/edit-indicator" class="fa fa-pencil-square<!-- IF privileges.posts:history --> pointer<!-- END --> edit-icon <!-- IF !posts.editor.username -->hidden<!-- ENDIF !posts.editor.username -->"></i>
<span data-editor="{posts.editor.remoteId}" component="post/editor" class="hidden">[[global:last-edited-by, {posts.editor.username}]] <span class="timeago" title="{isoTimeToLocaleString(posts.editedISO, config.userLang)}"></span></span>
<span class="visible-xs-inline-block visible-sm-inline-block visible-md-inline-block visible-lg-inline-block">
<a class="permalink text-muted" href="{config.relative_path}/post/{posts.pid}"><span class="timeago" title="{posts.timestampISO}"></span></a>
</span>
</span>
<span class="bookmarked"><i class="fa fa-bookmark-o"></i></span>
</div>
</div>
</small>
</div>
<br />
<div class="content" component="post/content" itemprop="text">
{posts.content}
</div>
<div class="post-footer">
{{{ if posts.user.signature }}}
<div component="post/signature" data-uid="{posts.user.uid}" class="post-signature">{posts.user.signature}</div>
{{{ end }}}
<div class="clearfix">
{{{ if !hideReplies }}}
<a component="post/reply-count" data-target-component="post/replies/container" href="#" class="threaded-replies user-select-none float-start text-muted {{{ if (!./replies || shouldHideReplyContainer(@value)) }}}hidden{{{ end }}}">
<span component="post/reply-count/avatars" class="avatars d-inline-flex gap-1 align-items-top hidden-xs {{{ if posts.replies.hasMore }}}hasMore{{{ end }}}">
{{{each posts.replies.users}}}
<span>{buildAvatar(posts.replies.users, "16px", true, "")}</span>
{{{end}}}
{{{ if posts.replies.hasMore}}}
<span><i class="fa fa-ellipsis"></i></span>
{{{ end }}}
</span>
<span class="replies-count small" component="post/reply-count/text" data-replies="{posts.replies.count}">{posts.replies.text}</span>
<span class="replies-last hidden-xs small">[[topic:last-reply-time]] <span class="timeago" title="{posts.replies.timestampISO}"></span></span>
<i class="fa fa-fw fa-chevron-down" component="post/replies/open"></i>
</a>
{{{ end }}}
<small class="d-flex justify-content-end align-items-center gap-1" component="post/actions">
<!-- IMPORT partials/topic/reactions.tpl -->
<span class="post-tools">
<a component="post/reply" href="#" class="btn btn-sm btn-link user-select-none <!-- IF !privileges.topics:reply -->hidden<!-- ENDIF !privileges.topics:reply -->">[[topic:reply]]</a>
<a component="post/quote" href="#" class="btn btn-sm btn-link user-select-none <!-- IF !privileges.topics:reply -->hidden<!-- ENDIF !privileges.topics:reply -->">[[topic:quote]]</a>
</span>
{{{ if ./announces }}}
<a component="post/announce-count" href="#" class="btn-ghost-sm" title="[[topic:announcers]]"><i class="fa fa-share-alt text-primary"></i> {./announces}</a>
{{{ end }}}
<!-- IF !reputation:disabled -->
<span class="votes">
<a component="post/upvote" href="#" class="btn btn-sm btn-link <!-- IF posts.upvoted -->upvoted<!-- ENDIF posts.upvoted -->">
<i class="fa fa-chevron-up"></i>
</a>
<span class="btn btn-sm btn-link" component="post/vote-count" data-votes="{posts.votes}">{posts.votes}</span>
<!-- IF !downvote:disabled -->
<a component="post/downvote" href="#" class="btn btn-sm btn-link <!-- IF posts.downvoted -->downvoted<!-- ENDIF posts.downvoted -->">
<i class="fa fa-chevron-down"></i>
</a>
<!-- ENDIF !downvote:disabled -->
</span>
<!-- ENDIF !reputation:disabled -->
<!-- IMPORT partials/topic/post-menu.tpl -->
</small>
</div>
<div component="post/replies/container"></div>
</div>
{{{ if (!./index && widgets.mainpost-footer.length) }}}
<div data-widget-area="mainpost-footer">
{{{ each widgets.mainpost-footer }}}
{widgets.mainpost-footer.html}
{{{ end }}}
</div>
{{{ end }}}

View File

@ -0,0 +1,27 @@
{{{ if privileges.topics:reply }}}
<div component="topic/quickreply/container" class="quick-reply d-flex gap-3 mb-4">
<div class="icon hidden-xs">
<a class="d-inline-block position-relative" href="{{{ if loggedInUser.remoteId }}}{config.relative_path}/user/{loggedInUser.remoteId}{{{ else }}}#{{{ end }}}">
{buildAvatar(loggedInUser, "48px", true, "", "user/picture")}
</a>
</div>
<form class="flex-grow-1 d-flex flex-column gap-2" method="post" action="{config.relative_path}/compose">
<input type="hidden" name="tid" value="{tid}" />
<input type="hidden" name="_csrf" value="{config.csrf_token}" />
<div class="quickreply-message position-relative">
<textarea rows="4" name="content" component="topic/quickreply/text" class="form-control mousetrap" placeholder="[[modules:composer.textarea.placeholder]]"></textarea>
<div class="imagedrop"><div>[[topic:composer.drag-and-drop-images]]</div></div>
</div>
<div>
<div class="d-flex justify-content-end gap-2">
<button type="button" component="topic/quickreply/upload/button" class="btn btn-ghost btn-sm border"><i class="fa fa-upload"></i></button>
<button type="button" component="topic/quickreply/expand" class="btn btn-ghost btn-sm border" title="[[topic:open-composer]]"><i class="fa fa-expand"></i></button>
<button type="submit" component="topic/quickreply/button" class="btn btn-sm btn-primary">[[topic:post-quick-reply]]</button>
</div>
</div>
</form>
<form class="d-none" component="topic/quickreply/upload" method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple class="hidden"/>
</form>
</div>
{{{ end }}}

View File

@ -0,0 +1,131 @@
<ul component="category" class="topics-list list-unstyled" itemscope itemtype="http://www.schema.org/ItemList" data-nextstart="{nextStart}" data-set="{set}">
{{{ each topics }}}
<li component="category/topic" class="category-item hover-parent py-2 mb-2 d-flex flex-column flex-lg-row align-items-start {function.generateTopicClass}" <!-- IMPORT partials/data/category.tpl -->>
<link itemprop="url" content="{config.relative_path}/topic/{./slug}" />
<meta itemprop="name" content="{function.stripTags, ./title}" />
<meta itemprop="itemListOrder" content="descending" />
<meta itemprop="position" content="{increment(./index, "1")}" />
<a id="{./index}" data-index="{./index}" component="topic/anchor"></a>
<div class="d-flex p-0 col-12 col-lg-7 gap-2 gap-lg-3 pe-1 align-items-start {{{ if config.theme.mobileTopicTeasers }}}mb-2 mb-lg-0{{{ end }}}">
<div class="flex-shrink-0 position-relative">
<a class="d-inline-block text-decoration-none avatar-tooltip" title="{./user.displayname}" href="{{{ if ./user.remoteId }}}{config.relative_path}/user/{./user.remoteId}{{{ else }}}#{{{ end }}}">
{buildAvatar(./user, "40px", true)}
</a>
{{{ if showSelect }}}
<div class="checkbox position-absolute top-100 start-50 translate-middle-x m-0 d-none d-lg-flex" style="max-width:max-content">
<i component="topic/select" class="fa text-muted pointer fa-square-o p-1 hover-visible"></i>
</div>
{{{ end }}}
</div>
<div class="flex-grow-1 d-flex flex-wrap gap-1 position-relative">
<h3 component="topic/header" class="title text-break fs-5 fw-semibold m-0 tracking-tight w-100 {{{ if showSelect }}}me-4 me-lg-0{{{ end }}}">
<a class="text-reset" href="{{{ if topics.noAnchor }}}#{{{ else }}}{config.relative_path}/topic/{./slug}{{{ if ./bookmark }}}/{./bookmark}{{{ end }}}{{{ end }}}">{./title}</a>
</h3>
<div component="topic/labels" class="d-flex flex-wrap gap-1 w-100 align-items-center">
<span component="topic/watched" class="badge border border-gray-300 text-body {{{ if !./followed }}}hidden{{{ end }}}">
<i class="fa fa-bell-o"></i>
<span>[[topic:watching]]</span>
</span>
<span component="topic/ignored" class="badge border border-gray-300 text-body {{{ if !./ignored }}}hidden{{{ end }}}">
<i class="fa fa-eye-slash"></i>
<span>[[topic:ignoring]]</span>
</span>
<span component="topic/scheduled" class="badge border border-gray-300 text-body {{{ if !./scheduled }}}hidden{{{ end }}}">
<i class="fa fa-clock-o"></i>
<span>[[topic:scheduled]]</span>
</span>
<span component="topic/pinned" class="badge border border-gray-300 text-body {{{ if (./scheduled || !./pinned) }}}hidden{{{ end }}}">
<i class="fa fa-thumb-tack"></i>
<span>{{{ if !./pinExpiry }}}[[topic:pinned]]{{{ else }}}[[topic:pinned-with-expiry, {isoTimeToLocaleString(./pinExpiryISO, config.userLang)}]]{{{ end }}}</span>
</span>
<span component="topic/locked" class="badge border border-gray-300 text-body {{{ if !./locked }}}hidden{{{ end }}}">
<i class="fa fa-lock"></i>
<span>[[topic:locked]]</span>
</span>
<span component="topic/moved" class="badge border border-gray-300 text-body {{{ if !./oldCid }}}hidden{{{ end }}}">
<i class="fa fa-arrow-circle-right"></i>
<span>[[topic:moved]]</span>
</span>
{{{each ./icons}}}<span class="lh-1">{@value}</span>{{{end}}}
{{{ if !template.category }}}
{function.buildCategoryLabel, ./category, "a", "border"}
{{{ end }}}
<span data-tid="{./tid}" component="topic/tags" class="lh-1 tag-list d-flex flex-wrap gap-1 {{{ if !./tags.length }}}hidden{{{ end }}}">
{{{ each ./tags }}}
<a href="{config.relative_path}/tags/{./valueEncoded}"><span class="badge border border-gray-300 fw-normal tag tag-class-{./class}" data-tag="{./value}">{./valueEscaped}</span></a>
{{{ end }}}
</span>
<div class="d-flex gap-1 d-block d-lg-none w-100">
<span class="badge text-body border stats text-xs text-muted">
<i class="fa-regular fa-fw fa-message"></i>
<span component="topic/post-count" class="fw-normal">{humanReadableNumber(./postcount, 0)}</span>
</span>
<a href="{config.relative_path}/topic/{./slug}{{{ if (./teaser.timestampISO && !config.theme.mobileTopicTeasers) }}}/{./teaser.index}{{{ end }}}" class="border badge bg-transparent text-muted fw-normal timeago" title="{{{ if (./teaser.timestampISO && !config.theme.mobileTopicTeasers) }}}{./teaser.timestampISO}{{{ else }}}{./timestampISO}{{{ end }}}"></a>
</div>
<a href="{config.relative_path}/topic/{./slug}" class="d-none d-lg-block badge bg-transparent text-muted fw-normal timeago" title="{./timestampISO}"></a>
</div>
{{{ if showSelect }}}
<div class="checkbox position-absolute top-0 end-0 m-0 d-flex d-lg-none" style="max-width:max-content">
<i component="topic/select" class="fa fa-square-o text-muted pointer p-1"></i>
</div>
{{{ end }}}
</div>
{{{ if ./thumbs.length }}}
<a class="topic-thumbs position-relative text-decoration-none flex-shrink-0 d-none d-xl-block" href="{config.relative_path}/topic/{./slug}{{{ if ./bookmark }}}/{./bookmark}{{{ end }}}" aria-label="[[topic:thumb-image]]">
<img class="topic-thumb rounded-1 bg-light" style="width:auto;max-width: 5.33rem;height: 3.33rem;object-fit: contain;" src="{./thumbs.0.url}" alt=""/>
<span data-numthumbs="{./thumbs.length}" class="px-1 position-absolute bottom-0 end-0 badge rounded-0 border fw-semibold text-bg-light" style="z-index: 1; border-top-left-radius: 0.25rem!important; border-bottom-right-radius: 0.25rem!important;">{./thumbs.length}</span>
</a>
{{{ end }}}
</div>
<div class="d-flex p-0 col-lg-5 col-12 align-content-stretch">
<div class="meta stats d-none d-lg-grid col-6 gap-1 pe-2 text-muted" style="grid-template-columns: 1fr 1fr 1fr;">
{{{ if !reputation:disabled }}}
<div class="stats-votes overflow-hidden d-flex flex-column align-items-center">
<span class="fs-4" title="{./votes}">{humanReadableNumber(./votes, 0)}</span>
<span class="d-none d-xl-flex text-uppercase text-xs">[[global:votes]]</span>
<i class="d-xl-none fa fa-fw text-xs text-muted opacity-75 fa-chevron-up"></i>
</div>
{{{ end }}}
<div class="stats-postcount overflow-hidden d-flex flex-column align-items-center">
<span class="fs-4" title="{./postcount}">{humanReadableNumber(./postcount, 0)}</span>
<span class="d-none d-xl-flex text-uppercase text-xs">[[global:posts]]</span>
<i class="d-xl-none fa-regular fa-fw text-xs text-muted opacity-75 fa-message"></i>
</div>
<div class="stats-viewcount overflow-hidden d-flex flex-column align-items-center">
<span class="fs-4" title="{./viewcount}">{humanReadableNumber(./viewcount, 0)}</span>
<span class="d-none d-xl-flex text-uppercase text-xs">[[global:views]]</span>
<i class="d-xl-none fa fa-fw text-xs text-muted opacity-75 fa-eye"></i>
</div>
</div>
<div component="topic/teaser" class="meta teaser col-lg-6 col-12 {{{ if !config.theme.mobileTopicTeasers }}}d-none d-lg-block{{{ end }}}">
<div class="lastpost border-start border-4 lh-sm h-100 d-flex flex-column gap-1" style="border-color: {./category.bgColor}!important;">
{{{ if ./unreplied }}}
<div class="ps-2 text-xs">
[[category:no-replies]]
</div>
{{{ else }}}
{{{ if ./teaser.pid }}}
<div class="ps-2">
<a href="{{{ if ./teaser.user.remoteId }}}{config.relative_path}/user/{./teaser.user.remoteId}{{{ else }}}#{{{ end }}}" class="text-decoration-none avatar-tooltip" title="{./teaser.user.displayname}">{buildAvatar(./teaser.user, "18px", true)}</a>
<a class="permalink text-muted timeago text-xs" href="{config.relative_path}/topic/{./slug}/{./teaser.index}" title="{./teaser.timestampISO}" aria-label="[[global:lastpost]]"></a>
</div>
<div class="post-content text-xs ps-2 line-clamp-sm-2 lh-sm text-break position-relative flex-fill">
<a class="stretched-link" tabindex="-1" href="{config.relative_path}/topic/{./slug}/{./teaser.index}" aria-label="[[global:lastpost]]"></a>
{./teaser.content}
</div>
{{{ end }}}
{{{ end }}}
</div>
</div>
</div>
</li>
{{{end}}}
</ul>

View File

@ -0,0 +1,41 @@
<li class="users-box registered-user text-center pb-3" data-uid="{users.uid}" style="width: 102px;">
<a href="{config.relative_path}/user/{users.remoteId}">{buildAvatar(users, "64px", true)}</a>
<div class="user-info">
<div class="text-nowrap text-truncate">
<a href="{config.relative_path}/user/{users.remoteId}">{users.username}</a>
</div>
<!-- IF section_online -->
<div class="lastonline">
<span class="timeago" title="{users.lastonlineISO}"></span>
</div>
<!-- ENDIF section_online -->
<!-- IF section_joindate -->
<div class="joindate">
<span class="timeago" title="{users.joindateISO}"></span>
</div>
<!-- ENDIF section_joindate -->
<!-- IF section_sort-reputation -->
<div class="reputation">
<i class="fa fa-star"></i>
<span>{formattedNumber(users.reputation)}</span>
</div>
<!-- ENDIF section_sort-reputation -->
<!-- IF section_sort-posts -->
<div class="post-count">
<i class="fa fa-pencil"></i>
<span>{formattedNumber(users.postcount)}</span>
</div>
<!-- ENDIF section_sort-posts -->
<!-- IF section_flagged -->
<div class="flag-count">
<i class="fa fa-flag"></i>
<span><a href="{config.relative_path}/flags?targetUid={users.uid}">{users.flags}</a></span>
</div>
<!-- ENDIF section_flagged -->
</div>
</li>

5
templates/templates.md Normal file
View File

@ -0,0 +1,5 @@
You can override templates from Harmony theme by placing your tpl files in this folder.
Use the same filename as the file you want to override. For example if you want to override recent.tpl name your file recent.tpl. If you want to override groups/list.tpl name your file groups/list.tpl and so on.
You can also create entirely new templates and render them in an express route with `res.render('/path/to/yourtemplate', dataToUse);`

127
templates/topic.tpl Normal file
View File

@ -0,0 +1,127 @@
<div data-widget-area="header">
{{{each widgets.header}}}
{{widgets.header.html}}
{{{end}}}
</div>
<div class="row mb-5">
<div class="topic {{{ if widgets.sidebar.length }}}col-lg-9 col-sm-12{{{ else }}}col-lg-12{{{ end }}}" itemid="{url}" itemscope itemtype="https://schema.org/DiscussionForumPosting">
<meta itemprop="headline" content="{escape(titleRaw)}">
<meta itemprop="text" content="{escape(titleRaw)}">
<meta itemprop="url" content="{url}">
<meta itemprop="datePublished" content="{timestampISO}">
<meta itemprop="dateModified" content="{lastposttimeISO}">
<div itemprop="author" itemscope itemtype="https://schema.org/Person">
<meta itemprop="name" content="{author.username}">
{{{ if author.remoteId }}}<meta itemprop="url" content="{config.relative_path}/user/{author.remoteId}">{{{ end }}}
</div>
<div class="topic-header sticky-top mb-3 bg-body">
<div class="d-flex flex-wrap gap-3 border-bottom p-2">
<div class="d-flex flex-column gap-2 flex-grow-1">
<h1 component="post/header" class="mb-0" itemprop="name">
<div class="topic-title d-flex">
<span class="fs-3" component="topic/title">{title}</span>
</div>
</h1>
<div class="topic-info d-flex gap-2 align-items-center flex-wrap">
<span component="topic/labels" class="d-flex gap-2 {{{ if (!scheduled && (!pinned && (!locked && (!icons.length && (!oldCid || (oldCid == "-1")))))) }}}hidden{{{ end }}}">
<span component="topic/scheduled" class="badge badge border border-gray-300 text-body {{{ if !scheduled }}}hidden{{{ end }}}">
<i class="fa fa-clock-o"></i> [[topic:scheduled]]
</span>
<span component="topic/pinned" class="badge badge border border-gray-300 text-body {{{ if (scheduled || !pinned) }}}hidden{{{ end }}}">
<i class="fa fa-thumb-tack"></i> {{{ if !pinExpiry }}}[[topic:pinned]]{{{ else }}}[[topic:pinned-with-expiry, {isoTimeToLocaleString(./pinExpiryISO, config.userLang)}]]{{{ end }}}
</span>
<span component="topic/locked" class="badge badge border border-gray-300 text-body {{{ if !locked }}}hidden{{{ end }}}">
<i class="fa fa-lock"></i> [[topic:locked]]
</span>
<a component="topic/moved" href="{config.relative_path}/category/{oldCid}" class="badge badge border border-gray-300 text-body text-decoration-none {{{ if !oldCid }}}hidden{{{ end }}}">
<i class="fa fa-arrow-circle-right"></i> {{{ if privileges.isAdminOrMod }}}[[topic:moved-from, {oldCategory.name}]]{{{ else }}}[[topic:moved]]{{{ end }}}
</a>
{{{each icons}}}<span class="lh-1">{@value}</span>{{{end}}}
</span>
{function.buildCategoryLabel, category, "a", "border"}
<div data-tid="{./tid}" component="topic/tags" class="lh-1 tags tag-list d-flex flex-wrap hidden-xs hidden-empty gap-2"><!-- IMPORT partials/topic/tags.tpl --></div>
<div class="d-flex hidden-xs gap-2"><!-- IMPORT partials/topic/stats.tpl --></div>
{{{ if !feeds:disableRSS }}}
{{{ if rssFeedUrl }}}<a class="hidden-xs" target="_blank" href="{rssFeedUrl}"><i class="fa fa-rss-square"></i></a>{{{ end }}}
{{{ end }}}
{{{ if browsingUsers }}}
<div class="d-inline-block hidden-xs">
<!-- IMPORT partials/topic/browsing-users.tpl -->
</div>
{{{ end }}}
<div class="ms-auto">
<!-- IMPORT partials/post_bar.tpl -->
</div>
</div>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center hidden-empty" component="topic/thumb/list"><!-- IMPORT partials/topic/thumbs.tpl --></div>
</div>
</div>
{{{ if merger }}}
<!-- IMPORT partials/topic/merged-message.tpl -->
{{{ end }}}
{{{ if forker }}}
<!-- IMPORT partials/topic/forked-message.tpl -->
{{{ end }}}
{{{ if !scheduled }}}
<!-- IMPORT partials/topic/deleted-message.tpl -->
{{{ end }}}
<ul component="topic" class="posts timeline" data-tid="{tid}" data-cid="{cid}">
{{{each posts}}}
<li component="post" class="{{{ if posts.deleted }}}deleted{{{ end }}} {{{ if posts.selfPost }}}self-post{{{ end }}} {{{ if posts.topicOwnerPost }}}topic-owner-post{{{ end }}}" <!-- IMPORT partials/data/topic.tpl -->>
<a component="post/anchor" data-index="{./index}" id="{increment(./index, "1")}"></a>
<meta itemprop="datePublished" content="{posts.timestampISO}">
<meta itemprop="dateModified" content="{posts.editedISO}">
<!-- IMPORT partials/topic/post.tpl -->
</li>
{{{ if (config.topicPostSort != "most_votes") }}}
{{{ each ./events}}}
<!-- IMPORT partials/topic/event.tpl -->
{{{ end }}}
{{{ end }}}
{{{end}}}
</ul>
{{{ if browsingUsers }}}
<div class="visible-xs">
<!-- IMPORT partials/topic/browsing-users.tpl -->
<hr/>
</div>
{{{ end }}}
{{{ if config.enableQuickReply }}}
<!-- IMPORT partials/topic/quickreply.tpl -->
{{{ end }}}
{{{ if config.usePagination }}}
<!-- IMPORT partials/paginator.tpl -->
{{{ end }}}
<!-- IMPORT partials/topic/navigator.tpl -->
</div>
<div data-widget-area="sidebar" class="col-lg-3 col-sm-12 {{{ if !widgets.sidebar.length }}}hidden{{{ end }}}">
{{{each widgets.sidebar}}}
{{widgets.sidebar.html}}
{{{end}}}
</div>
</div>
<div data-widget-area="footer">
{{{each widgets.footer}}}
{{widgets.footer.html}}
{{{end}}}
</div>
{{{ if !config.usePagination }}}
<noscript>
<!-- IMPORT partials/paginator.tpl -->
</noscript>
{{{ end }}}

View File

@ -1,6 +1,7 @@
{ {
"id": "nodebb-theme-quickstart", "id": "nodebb-theme-rockfic",
"name": "My Theme Name", "name": "Rockfic",
"description": "Enter a description here", "description": "Theme used on rockfic.com",
"url": "https://github.com/nodebb/nodebb-theme-quickstart" "url": "https://github.com/nodebb/nodebb-theme-quickstart",
"baseTheme": "nodebb-theme-persona"
} }

View File

@ -1 +1,4 @@
// override harmony font-path
$font-path: "./plugins/nodebb-theme-quickstart";
@import "../nodebb-theme-harmony/theme"; @import "../nodebb-theme-harmony/theme";