* A bunch of shorthand to put together common parts of an HTML page.
*/
+const { lazy } = require('@squeep/lazy-property');
+
+
+/**
+ * Set up expected fields for how we handle error reporting
+ * and whatnot.
+ * @param {Object} ctx
+ */
+const initContext = (ctx) => {
+ ctx.errors = [];
+ ctx.notifications = [];
+};
+
/**
* Some fields may have values outside normal dates, handle them here.
* @param {Date} date
};
+/**
+ * Why is rendering a Date as a string this complicated?
+ * We handle the infinities because pg-promise might provide those in
+ * lieu of a Date object from timestamp fields.
+ * @param {Date|Number|String} date
+ * @param {String=} pInf
+ * @param {String=} nInf
+ * @param {String=} otherwise
+ */
+const dateFormat = (date, pInf = 'Never', nInf = 'Forever', otherwise = '') => {
+ const isDatableType = ['number', 'string'].includes(typeof date);
+ switch (date) {
+ case Infinity:
+ return pInf;
+ case -Infinity:
+ return nInf;
+ default:
+ if (!date
+ || Number.isNaN(date.valueOf())
+ || (!(date instanceof Date) && !isDatableType)) {
+ return otherwise;
+ }
+ }
+ if (isDatableType) {
+ date = new Date(date);
+ }
+ const parts = dateFormat._dtf.formatToParts(date);
+ return parts.map((p) => p.value).join('');
+};
+lazy(dateFormat, '_dtf', () => {
+ const dateTimeFormatOptions = {
+ dateStyle: 'medium',
+ timeStyle: 'long',
+ };
+ return new Intl.DateTimeFormat(undefined, dateTimeFormatOptions);
+});
+
+
+/**
+ * Wrap a Date in a <time> block.
+ * @param {Date} date
+ * @param {Object} options
+ * @param {String=} options.title
+ */
+const timeElement = (date, options = {}) => {
+ const {
+ title,
+ pInf,
+ nInf,
+ otherwise,
+ } = options;
+ const attributes = {
+ ...(title && { title }),
+ ...(date instanceof Date && { datetime: date.toISOString() }),
+ };
+ return [
+ '<time',
+ elementAttributes(attributes),
+ '>',
+ dateFormat(date, pInf, nInf, otherwise),
+ '</time>',
+ ].join('');
+};
+
+
/**
* Render a duration.
* @param {Number} seconds
result.push(`${r} ${label}${r != 1 ? 's' : ''}`);
}
value = factor ? Math.floor(value / factor) : value;
- }
+ };
nextResult(60, 'second');
nextResult(60, 'minute');
};
+/**
+ * Return array of strings prefixed with tabs.
+ * @param {Number} indent
+ * @param {String[]} list
+ */
+const indented = (indent, list) => {
+ const spacer = '\t'.repeat(indent);
+ return list.map((l) => `${spacer}${l}`);
+};
+
+
/**
* Render the preamble <head> for an HTML page.
* @param {Number} pagePathLevel number of paths below root this page is
} = options;
return `\t<head>
\t\t<meta charset="utf-8">
+\t\t<meta name="viewport" content="width=device-width,initial-scale=1">
\t\t<link rel="stylesheet" href="${rootPathPfx}static/theme.css">
\t\t<link rel="stylesheet" href="${rootPathPfx}static/custom.css">
${headElements.map((e) => '\t\t' + e).join('\n')}
const {
bodyAttributes = {},
} = options;
+ const firefoxFix = '\n<script>0</script>'; // This fixes a layout rendering flash on load in Firefox; do not know why this works, but it does.
return `
-\t<body${elementAttributes(bodyAttributes)}>
+\t<body${elementAttributes(bodyAttributes)}>${firefoxFix}
${htmlHeader(pagePathLevel, ctx, options)}
${htmlMessages(ctx, options)}
\t\t<main>
* @param {Object} ctx
* @param {Object} options
* @param {String[]=} options.logoUrl
+ * @param {String[]=} options.logoAlt
* @param {String[]=} options.pageTitle
* @returns {String}
*/
function htmlHeader(pagePathLevel, ctx, options) {
- const rootPathPfx = '../'.repeat(Math.max(pagePathLevel - 1, 0));
+ const rootPathPfx = '../'.repeat(pagePathLevel);
const {
logoUrl = '',
+ logoAlt = 'logo',
pageTitle = '',
} = options;
- const logoImg = logoUrl ? `<img src="${rootPathPfx}${logoUrl}" class="logo">` : '';
+ const logoImg = logoUrl ? `<img src="${rootPathPfx}${logoUrl}" alt="logo" class="${logoAlt}">` : '';
return `\t\t<header>
\t\t\t<h1>${logoImg}${pageTitle}</h1>
${htmlNav(ctx, options)}
* @param {String[]=} options.notificationContent
*/
function htmlMessages(ctx, options) {
- const errorHeading = options && options.errorHeading ? `
+ const errorHeading = options?.errorHeading ? `
\t<h2>${options.errorHeading}</h2>` : '';
- const errorContent = options && options.errorContent && options.errorContent.length ? '\n' + options.errorContent.map(((content) => `\t${content}`)).join('\n') : '';
- const notificationHeading = options && options.notificationHeading ? `\n\t<h2>${options.notificationHeading}</h2>` : '';
- const notificationContent = options && options.notificationContent && options.notificationContent.length ? '\n' + options.notificationContent.map(((content) => `\t${content}`)).join('\n') : '';
- const errors = ctx && ctx.errors && ctx.errors.length ? `
+ const errorContent = options?.errorContent?.length ? '\n' + options.errorContent.map(((content) => `\t${content}`)).join('\n') : '';
+ const notificationHeading = options?.notificationHeading ? `\n\t<h2>${options.notificationHeading}</h2>` : '';
+ const notificationContent = options?.notificationContent?.length ? '\n' + options.notificationContent.map(((content) => `\t${content}`)).join('\n') : '';
+ const errors = ctx?.errors?.length ? `
<section class="error">${errorHeading}${errorContent}
${UL(ctx.errors, 1)}
</section>` : '';
- const notifications = ctx && ctx.notifications && ctx.notifications.length ? `
+ const notifications = ctx?.notifications?.length ? `
<section class="notification">${notificationHeading}${notificationContent}
${UL(ctx.notifications, 1)}
</section>` : '';
* @param {Object} options
* @param {String=} options.pageTitle
* @param {String=} options.logoUrl
+ * @param {String=} options.logoAlt
* @param {Object[]=} options.bodyAttributes
* @param {String[]=} options.headElements
* @param {Object[]=} options.navLinks
* @returns {String}
*/
function htmlPage(pagePathLevel, ctx, options, main = []) {
- const user = (ctx && ctx.session && ctx.session.authenticatedProfile) || (ctx && ctx.session && ctx.session.authenticatedIdentifier);
+ const user = ctx?.session?.authenticatedProfile || ctx?.session?.authenticatedIdentifier;
if (user) {
if (!options.navLinks) {
options.navLinks = [];
}
- const logoutRedirect = ctx && ctx.url ? `?r=${encodeURIComponent(ctx.url)}` : '';
- const logoutPath = (pagePathLevel > 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/';
+ const logoutRedirect = ctx?.url ? `?r=${encodeURIComponent(ctx.url)}` : '';
+ const adminPath = (pagePathLevel > 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/';
options.navLinks.push({
text: `Logout (${user})`,
- href: `${logoutPath}logout${logoutRedirect}`,
+ href: `${adminPath}logout${logoutRedirect}`,
+ }, {
+ text: 'Settings',
+ href: `${adminPath}settings`,
});
}
module.exports = {
+ initContext,
dateOrNot,
+ dateFormat,
+ timeElement,
secondsToPeriod,
htmlHead,
htmlBody,
htmlHeader,
htmlFooter,
htmlMessages,
+ indented,
renderNavLink,
LI,
UL,