X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Ftemplate-helper.js;h=e48cdb4fc5e59e17e5179b35a0339f357ba657be;hb=858b76483eabfc25b60f7bdf7ef4be97b61b5b41;hp=122a3a09783b8aad2306d238bb134dc02bfb6c47;hpb=1d150b8050db306991685cfbd294ef8f3b714fca;p=squeep-html-template-helper diff --git a/lib/template-helper.js b/lib/template-helper.js index 122a3a0..e48cdb4 100644 --- a/lib/template-helper.js +++ b/lib/template-helper.js @@ -4,6 +4,19 @@ * 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 @@ -25,6 +38,71 @@ const dateOrNot = (date, otherwise) => { }; +/** + * 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 ', + ].join(''); +}; + + /** * Render a duration. * @param {Number} seconds @@ -40,7 +118,7 @@ const secondsToPeriod = (seconds) => { result.push(`${r} ${label}${r != 1 ? 's' : ''}`); } value = factor ? Math.floor(value / factor) : value; - } + }; nextResult(60, 'second'); nextResult(60, 'minute'); @@ -53,6 +131,17 @@ const secondsToPeriod = (seconds) => { }; +/** + * 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 for an HTML page. * @param {Number} pagePathLevel number of paths below root this page is @@ -70,6 +159,7 @@ function htmlHead(pagePathLevel, ctx, options) { } = options; return `\t \t\t +\t\t \t\t \t\t ${headElements.map((e) => '\t\t' + e).join('\n')} @@ -91,8 +181,9 @@ function htmlBody(pagePathLevel, ctx, options, main = []) { const { bodyAttributes = {}, } = options; + const firefoxFix = '\n'; // This fixes a layout rendering flash on load in Firefox; do not know why this works, but it does. return ` -\t +\t${firefoxFix} ${htmlHeader(pagePathLevel, ctx, options)} ${htmlMessages(ctx, options)} \t\t
@@ -141,16 +232,18 @@ ${spacer}` : ''; * @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 ? `` : ''; + const logoImg = logoUrl ? `logo` : ''; return `\t\t
\t\t\t

${logoImg}${pageTitle}

${htmlNav(ctx, options)} @@ -247,16 +340,16 @@ ${spacer}`; * @param {String[]=} options.notificationContent */ function htmlMessages(ctx, options) { - const errorHeading = options && options.errorHeading ? ` + const errorHeading = options?.errorHeading ? ` \t

${options.errorHeading}

` : ''; - const errorContent = options && options.errorContent && options.errorContent.length ? '\n' + options.errorContent.map(((content) => `\t${content}`)).join('\n') : ''; - const notificationHeading = options && options.notificationHeading ? `\n\t

${options.notificationHeading}

` : ''; - 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

${options.notificationHeading}

` : ''; + const notificationContent = options?.notificationContent?.length ? '\n' + options.notificationContent.map(((content) => `\t${content}`)).join('\n') : ''; + const errors = ctx?.errors?.length ? `
${errorHeading}${errorContent} ${UL(ctx.errors, 1)}
` : ''; - const notifications = ctx && ctx.notifications && ctx.notifications.length ? ` + const notifications = ctx?.notifications?.length ? `
${notificationHeading}${notificationContent} ${UL(ctx.notifications, 1)}
` : ''; @@ -276,6 +369,7 @@ ${UL(ctx.notifications, 1)} * @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 @@ -288,16 +382,19 @@ ${UL(ctx.notifications, 1)} * @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: 'Account', + href: `${adminPath}settings`, + }, { text: `Logout (${user})`, - href: `${logoutPath}logout${logoutRedirect}`, + href: `${adminPath}logout${logoutRedirect}`, }); } @@ -312,7 +409,10 @@ function htmlPage(pagePathLevel, ctx, options, main = []) { module.exports = { + initContext, dateOrNot, + dateFormat, + timeElement, secondsToPeriod, htmlHead, htmlBody, @@ -320,6 +420,7 @@ module.exports = { htmlHeader, htmlFooter, htmlMessages, + indented, renderNavLink, LI, UL,