4 * A bunch of shorthand to put together common parts of an HTML page.
8 * Some fields may have values outside normal dates, handle them here.
10 * @param {String} otherwise
12 const dateOrNot
= (date
, otherwise
) => {
16 if (typeof date
=== 'number') {
17 date
= new Date(date
);
19 const dateMs
= date
.getTime();
20 if (!Number
.isFinite(dateMs
)
24 return date
.toString();
30 * @param {Number} seconds
33 const secondsToPeriod
= (seconds
) => {
37 const nextResult
= (factor
, label
) => {
38 const r
= factor
? value
% factor : value
;
40 result
.push(`${r} ${label}${r != 1 ? 's' : ''}`);
42 value
= factor
? Math
.floor(value
/ factor
) : value
;
45 nextResult(60, 'second');
46 nextResult(60, 'minute');
47 nextResult(24, 'hour');
48 nextResult(30, 'day');
49 nextResult(undefined, 'month');
52 return result
.join(' ');
57 * Render the preamble <head> for an HTML page.
58 * @param {Number} pagePathLevel number of paths below root this page is
60 * @param {Object} options
61 * @param {String[]=} options.headElements
62 * @param {String=} options.pageTitle
65 function htmlHead(pagePathLevel
, ctx
, options
) {
66 const rootPathPfx
= '../'.repeat(pagePathLevel
);
72 \t\t<meta charset="utf-8">
73 \t\t<link rel="stylesheet" href="${rootPathPfx}static/theme.css">
74 \t\t<link rel="stylesheet" href="${rootPathPfx}static/custom.css">
75 ${headElements.map((e) => '\t\t' + e).join('\n')}
76 \t\t<title>${pageTitle}</title>
82 * Render the main content of an HTML page.
83 * @param {Number} pagePathLevel
85 * @param {Object} options
86 * @param {Object} options.bodyAttributes
87 * @param {String[]} main
90 function htmlBody(pagePathLevel
, ctx
, options
, main
= []) {
95 \t<body${elementAttributes(bodyAttributes)}>
96 ${htmlHeader(pagePathLevel, ctx, options)}
97 ${htmlMessages(ctx, options)}
101 ${htmlFooter(ctx, options)}
107 * Render a navigation link for the header section.
108 * @param {Object} nav
109 * @param {String} nav.href
110 * @param {String} nav.class
111 * @param {String} nav.text
114 function renderNavLink(nav
) {
115 return `<a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>`;
120 * Render the navigation section of the page header.
121 * @param {Object} ctx
122 * @param {Object} options
123 * @param {Object[]=} options.navLinks
126 function htmlNav(ctx
, options
) {
128 const spacer
= '\t'.repeat(indent
);
132 return navLinks
.length
? `${spacer}<nav>
133 ${OL(navLinks.map((link) => renderNavLink(link)), indent + 1)}
134 ${spacer}</nav>` : '';
139 * Render the banner and navigation header.
140 * @param {Number} pagePathLevel
141 * @param {Object} ctx
142 * @param {Object} options
143 * @param {String[]=} options.logoUrl
144 * @param {String[]=} options.pageTitle
147 function htmlHeader(pagePathLevel
, ctx
, options
) {
148 const rootPathPfx
= '../'.repeat(Math
.max(pagePathLevel
- 1, 0));
153 const logoImg
= logoUrl
? `<img src="${rootPathPfx}${logoUrl}" class="logo">` : '';
155 \t\t\t<h1>${logoImg}${pageTitle}</h1>
156 ${htmlNav(ctx, options)}
162 * Render the bottom boilerplate.
163 * @param {Object} ctx
164 * @param {Object} options
165 * @param {String[]} options.footerEntries
168 function htmlFooter(ctx
, options
) {
170 const spacer
= '\t'.repeat(indent
);
175 return footerEntries
.length
? `${spacer}<footer>
176 ${OL(footerEntries, indent + 1)}
177 ${spacer}</footer>` : '';
182 * Convert an object into element attributes.
183 * @param {Object} attributes
186 function elementAttributes(attributes
) {
187 const attr
= Object
.entries(attributes
).map(([name
, value
]) => `${name}="${value}"`).join(' ');
188 return attr
? ' ' + attr : '';
193 * Wrap an item in a list item element.
194 * @param {String} item
195 * @param {Number} indent
196 * @param {Object} attributes
199 function LI(item
, indent
= 0, attributes
= {}) {
200 const spacer
= '\t'.repeat(indent
);
201 return `${spacer}<li${elementAttributes(attributes)}>${item}</li>`;
206 * Wrap a list of items in an unordered list.
207 * @param {String[]} items
208 * @param {Number} indent
209 * @param {Object} attributes
210 * @param {(item) => Object} itemAttributeGenerator
213 function UL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
214 const spacer
= '\t'.repeat(indent
);
215 return `${spacer}<ul${elementAttributes(attributes)}>
216 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
222 * Wrap a list of items in an ordered list.
223 * @param {String[]} items
224 * @param {Number} indent
225 * @param {Object} attributes
226 * @param {(item) => Object} itemAttributeGenerator
230 function OL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
231 const spacer
= '\t'.repeat(indent
);
232 return `${spacer}<ol${elementAttributes(attributes)}>
233 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
239 * Show any error or notice messages from context.
240 * @param {Object} ctx
241 * @param {String[]=} ctx.errors
242 * @param {String[]=} ctx.notifications
243 * @param {Object} options
244 * @param {String=} options.errorHeading
245 * @param {String[]=} options.errorContent
246 * @param {String=} options.notificationHeading
247 * @param {String[]=} options.notificationContent
249 function htmlMessages(ctx
, options
) {
250 const errorHeading
= options
&& options
.errorHeading
? `
251 \t<h2>${options.errorHeading}</h2>` : '';
252 const errorContent
= options
&& options
.errorContent
&& options
.errorContent
.length
? '\n' + options
.errorContent
.map(((content
) => `\t${content}`)).join('\n') : '';
253 const notificationHeading
= options
&& options
.notificationHeading
? `\n\t<h2>${options.notificationHeading}</h2>` : '';
254 const notificationContent
= options
&& options
.notificationContent
&& options
.notificationContent
.length
? '\n' + options
.notificationContent
.map(((content
) => `\t${content}`)).join('\n') : '';
255 const errors
= ctx
&& ctx
.errors
&& ctx
.errors
.length
? `
256 <section class="error">${errorHeading}${errorContent}
259 const notifications
= ctx
&& ctx
.notifications
&& ctx
.notifications
.length
? `
260 <section class="notification">${notificationHeading}${notificationContent}
261 ${UL(ctx.notifications, 1)}
263 return [errors
, notifications
].join('\n');
268 * Render all parts of an HTML page. Adds user logout nav link automatically.
269 * @param {Number} pagePathLevel - relative path-distance to base
270 * @param {Object} ctx
271 * @param {Object=} ctx.session
272 * @param {String=} ctx.session.authenticatedIdentifier
273 * @param {String=} ctx.session.authenticatedProfile
274 * @param {String[]=} ctx.errors
275 * @param {String[]=} ctx.notifications
276 * @param {Object} options
277 * @param {String=} options.pageTitle
278 * @param {String=} options.logoUrl
279 * @param {Object[]=} options.bodyAttributes
280 * @param {String[]=} options.headElements
281 * @param {Object[]=} options.navLinks
282 * @param {String[]=} options.footerEntries
283 * @param {String=} options.errorHeading
284 * @param {String=} options.errorContent
285 * @param {String=} options.notificationHeading
286 * @param {String=} options.notificationContent
287 * @param {String[]} main - contents
290 function htmlPage(pagePathLevel
, ctx
, options
, main
= []) {
291 const user
= (ctx
&& ctx
.session
&& ctx
.session
.authenticatedProfile
) || (ctx
&& ctx
.session
&& ctx
.session
.authenticatedIdentifier
);
293 if (!options
.navLinks
) {
294 options
.navLinks
= [];
296 const logoutRedirect
= ctx
&& ctx
.url
? `?r=${encodeURIComponent(ctx.url)}` : '';
297 const logoutPath
= (pagePathLevel
> 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/';
298 options
.navLinks
.push({
299 text: `Logout (${user})`,
300 href: `${logoutPath}logout${logoutRedirect}`,
307 htmlHead(pagePathLevel
, ctx
, options
),
308 htmlBody(pagePathLevel
, ctx
, options
, main
),