#!/bin/sh
./set.mjs /api/globals/set.json <globals.json
-./set.mjs /api/nodemailers/set.json <nodemailers.json
./set.mjs /api/navigation/set.json <navigation.json
+#./set.mjs /api/nodemailers/set.json <nodemailers.json
"title": "14/01/2022",
"children": {},
"menu": [],
+ "images": [
+ "image.svg",
+ "image.svg",
+ "thumbnail.svg"
+ ],
"description": "Log-structured JSON database",
- "author": "Nick Downing",
- "image": "image.svg",
- "thumbnail": "thumbnail.svg"
+ "author": "Nick Downing"
},
"20220128": {
"title": "28/01/2022",
"children": {},
"menu": [],
+ "images": [
+ "image.svg",
+ "image.svg",
+ "thumbnail.svg"
+ ],
"description": "Refactoring",
- "author": "Nick Downing",
- "image": "image.svg",
- "thumbnail": "thumbnail.svg"
+ "author": "Nick Downing"
}
},
"menu": [
},
"projects": {
"title": "Projects",
- "children": {},
- "menu": []
+ "children": {
+ "jst": {
+ "title": "JST",
+ "description": "Create websites efficiently",
+ "children": {},
+ "menu": [],
+ "icon": "/_svg/icon_jst.svg"
+ },
+ "pitree": {
+ "title": "Ï€tree",
+ "description": "Create languages efficiently",
+ "children": {},
+ "menu": [],
+ "icon": "/_svg/icon_pitree.svg"
+ }
+ },
+ "menu": [
+ "jst",
+ "pitree"
+ ]
},
"sphinx": {
"title": "Documentation",
assert(this.database === undefined)
this.database = await this.resources.ref(
- 'database',
+ `database:${this.root}`,
async () => {
let database = new logjson.Database()
await database.open(this.root + '/database.logjson')
try {
let root = await transaction.get()
let p = await get_navigation(root, env.component_names)
+ let images = await p.get_json('images')
- div.row.mb-3 {
- div.col-sm-12 {
- img.img-responsive(
- src=await p.get_json('image') || 'image.jpg'
- ) {}
+ // we can use higher resolution images when lower not available
+ while (images.length < 2)
+ images.push(images[images.length - 1])
+
+ div.my-3 {
+ a(href=images[0]) {
+ img.img-fluid(src=images[1]) {}
}
}
return async (root, name) => {
let nodemailers = await root.get('nodemailers')
if (nodemailers === undefined)
- throw new Problem(
+ throw new jst_server.Problem(
'Nodemailer error',
'Please import the nodemailers data into the database.',
509
)
let _nodemailer = await nodemailers.get_json(name)
- throw new Problem(
+ if (_nodemailer === undefined)
+ throw new jst_server.Problem(
'Nodemailer error',
- `Can't find the nodemailer "${name}" in the nodemailers data.',
+ `Can't find the nodemailer "${name}" in the nodemailers data.`,
509
)
return nodemailer.createTransport(_nodemailer)
// note: menu_titles.length === menu_names.length + 1
// menu_titles[0] corresponds to /, is 'Home' or similar
- // menu_titles[i] corresponds to menu_names[i - 1], i >= 1a
+ // menu_titles[i] corresponds to menu_names[i - 1], i >= 1
// (navbar has Home appearing at same level as its immediate children)
await page(
// }
//}
}
- nav.navbar.navbar-expand-lg.navbar-dark.bg-primary.extend-background {
+ nav.navbar.navbar-expand-lg.navbar-dark.bg-primary.container-background {
//a.navbar-brand(href="#") {'Navbar'}
//' '
button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation") {
await body(_out)
- footer.page-footer.extend-background.py-5 {
+ footer.page-footer.container-background.py-5 {
a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
img(alt="Creative Commons License" style="border-width:0;" src="/images/by-sa_3.0_88x31.png") {}
}
span.h4.modal-title {'Sign in'}
}
div.modal-body {
- form#navbar-sign-in-form {
+ form.mb-2#navbar-sign-in-form {
div.row {
div.col-md-12 {
div.form-group {
}
}
- p.mt-2 {
+ p {
'No account yet? '
a(href="/my_account/sign_up/index.html") {'Sign up'}
}
a(href="/my_account/password_reset/index.html") {'Password reset'}
}
- p.'mt-3'.mb-0#navbar-sign-in-message(hidden) {}
+ div.alert.alert-danger.'mt-3'.mb-0#navbar-sign-in-alert(hidden) {}
}
div.modal-footer {
button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
div.row {
div.col-md-12 {
div.form-group {
- label.form-label(for="navbar-feedback-message1") {'Message'}
- textarea.form-control#navbar-feedback-message1(placeholder="I noticed that..." required rows=4 maxlength=65536) {
+ label.form-label(for="navbar-feedback-message") {'Message'}
+ textarea.form-control#navbar-feedback-message(placeholder="I noticed that..." required rows=4 maxlength=65536) {
if (feedback_draft)
`${feedback_draft.message}`
}
}
}
- p.'mt-3'.mb-0#navbar-feedback-message(hidden) {}
+ div.alert.alert-danger.'mt-3'.mb-0#navbar-feedback-alert(hidden) {}
}
div.modal-footer {
button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
document.addEventListener(
'DOMContentLoaded',
() => {
+ let id_navbar_feedback_alert = document.getElementById('navbar-feedback-alert')
let id_navbar_feedback_cross = document.getElementById('navbar-feedback-cross')
let id_navbar_feedback_form = document.getElementById('navbar-feedback-form')
let id_navbar_feedback_icon = document.getElementById('navbar-feedback-icon')
let id_navbar_feedback_message = document.getElementById('navbar-feedback-message')
- let id_navbar_feedback_message1 = document.getElementById('navbar-feedback-message1')
let id_navbar_feedback_modal = document.getElementById('navbar-feedback-modal')
let id_navbar_feedback_send_message = document.getElementById('navbar-feedback-send-message')
let id_navbar_feedback_spinner = document.getElementById('navbar-feedback-spinner')
let id_navbar_message_modal_message = document.getElementById('navbar-message-modal-message')
let id_navbar_search_button = document.getElementById('navbar-search-button')
let id_navbar_sign_in = document.getElementById('navbar-sign-in')
+ let id_navbar_sign_in_alert = document.getElementById('navbar-sign-in-alert')
let id_navbar_sign_in_cross = document.getElementById('navbar-sign-in-cross')
let id_navbar_sign_in_email = document.getElementById('navbar-sign-in-email')
let id_navbar_sign_in_form = document.getElementById('navbar-sign-in-form')
let id_navbar_sign_in_icon = document.getElementById('navbar-sign-in-icon')
- let id_navbar_sign_in_message = document.getElementById('navbar-sign-in-message')
let id_navbar_sign_in_modal = document.getElementById('navbar-sign-in-modal')
let id_navbar_sign_in_password = document.getElementById('navbar-sign-in-password')
let id_navbar_sign_in_sign_in = document.getElementById('navbar-sign-in-sign-in')
//id_navbar_sign_in_tick.hidden = true
id_navbar_sign_in_cross.hidden = true
id_navbar_sign_in_spinner.hidden = true
- id_navbar_sign_in_message.hidden = true
+ id_navbar_sign_in_alert.hidden = true
}
id_navbar_sign_in_email.addEventListener(
//id_navbar_sign_in_tick.hidden = true
id_navbar_sign_in_cross.hidden = true
id_navbar_sign_in_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_navbar_sign_in_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_navbar_sign_in_alert.hidden = true
if (!id_navbar_sign_in_form.checkValidity()) {
id_navbar_sign_in_form.classList.add('was-validated');
id_navbar_sign_in_cross.hidden = false
id_navbar_sign_in_spinner.hidden = true
- id_navbar_sign_in_message.textContent = problem.detail
- //id_navbar_sign_in_message.classList.remove('text-success')
- id_navbar_sign_in_message.classList.add('text-danger')
- id_navbar_sign_in_message.hidden = false
+ id_navbar_sign_in_alert.textContent = problem.detail
+ //id_navbar_sign_in_alert.classList.remove('alert-success')
+ //id_navbar_sign_in_alert.classList.add('alert-danger')
+ id_navbar_sign_in_alert.hidden = false
return
}
//id_navbar_sign_in_tick.hidden = false
//id_navbar_sign_in_spinner.hidden = true
- //id_navbar_sign_in_message.textContent = `You are now signed in as "${email}".`
- //id_navbar_sign_in_message.classList.add('text-success')
- //id_navbar_sign_in_message.classList.remove('text-danger')
- //id_navbar_sign_in_message.hidden = false
+ //id_navbar_sign_in_alert.textContent = `You are now signed in as "${email}".`
+ //id_navbar_sign_in_alert.classList.add('alert-success')
+ //id_navbar_sign_in_alert.classList.remove('alert-danger')
+ //id_navbar_sign_in_alert.hidden = false
if (sign_in_out(true))
// if location has been changed, leave the spinner and do
// not show status/dialog, as it causes an annoying flicker
return
- id_navbar_signed_in_status.textContent = 'Signed in.' //`Signed in as ${email}.`
+ id_navbar_signed_in_status.textContent = 'Signed in.'
id_navbar_sign_in.hidden = true
id_navbar_sign_up.hidden = true
id_navbar_sign_out.hidden = false
id_navbar_sign_in_icon.hidden = false
id_navbar_sign_in_spinner.hidden = true
- id_navbar_sign_in_message.hidden = true
+ id_navbar_sign_in_alert.hidden = true
id_navbar_message_modal_message.textContent = `You are now signed in as "${email}".`
$('#navbar-sign-in-modal').modal('hide')
$('#navbar-message-modal').modal('show')
// not show status/dialog, as it causes an annoying flicker
return
- id_navbar_signed_in_status.textContent = 'Browsing as guest.'
+ id_navbar_signed_in_status.textContent = 'Signed out.'
id_navbar_sign_in.hidden = false
id_navbar_sign_up.hidden = false
id_navbar_sign_out.hidden = true
'click',
() => {
// hack to move cursor to end of textarea
- let temp = id_navbar_feedback_message1.value
- id_navbar_feedback_message1.value = ''
- id_navbar_feedback_message1.value = temp
+ let temp = id_navbar_feedback_message.value
+ id_navbar_feedback_message.value = ''
+ id_navbar_feedback_message.value = temp
$('#navbar-feedback-modal').modal('show')
return false
$('#navbar-feedback-modal').on(
'shown.bs.modal',
- () => {id_navbar_feedback_message1.focus()}
+ () => {id_navbar_feedback_message.focus()}
)
let feedback_input_semaphore = new BinarySemaphore(false)
feedback_input_semaphore.try_acquire()
await api_call(
'/api/feedback/set_draft.json',
- id_navbar_feedback_message1.value.length === 0 ?
+ id_navbar_feedback_message.value.length === 0 ?
null :
{
- message: id_navbar_feedback_message1.value.slice(0, 65536)
+ message: id_navbar_feedback_message.value.slice(0, 65536)
}
)
}
feedback_input_semaphore.release()
id_navbar_feedback_send_message.disabled =
- id_navbar_feedback_message1.value.length === 0
+ id_navbar_feedback_message.value.length === 0
id_navbar_feedback_icon.hidden = false
//id_navbar_feedback_tick.hidden = true
id_navbar_feedback_cross.hidden = true
id_navbar_feedback_spinner.hidden = true
- id_navbar_feedback_message.hidden = true
+ id_navbar_feedback_alert.hidden = true
}
- id_navbar_feedback_message1.addEventListener(
+ id_navbar_feedback_message.addEventListener(
'input',
feedback_edited
)
//id_navbar_feedback_tick.hidden = true
id_navbar_feedback_cross.hidden = true
id_navbar_feedback_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_navbar_feedback_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_navbar_feedback_alert.hidden = true
if (!id_navbar_feedback_form.checkValidity()) {
id_navbar_feedback_form.classList.add('was-validated');
await api_call(
'/api/feedback/send_message.json',
location.href,
- id_navbar_feedback_message1.value.slice(0, 65536)
+ id_navbar_feedback_message.value.slice(0, 65536)
)
}
catch (error) {
id_navbar_feedback_cross.hidden = false
id_navbar_feedback_spinner.hidden = true
- id_navbar_feedback_message.textContent = problem.detail
- //id_navbar_feedback_message.classList.remove('text-success')
- id_navbar_feedback_message.classList.add('text-danger')
- id_navbar_feedback_message.hidden = false
+ id_navbar_feedback_alert.textContent = problem.detail
+ //id_navbar_feedback_alert.classList.remove('alert-success')
+ //id_navbar_feedback_alert.classList.add('alert-danger')
+ id_navbar_feedback_alert.hidden = false
return
}
//id_navbar_feedback_tick.hidden = false
//id_navbar_feedback_spinner.hidden = true
- //id_navbar_feedback_message.textContent = 'We have received your message. We will be in touch as soon as possible.'
- //id_navbar_feedback_message.classList.add('text-success')
- //id_navbar_feedback_message.classList.remove('text-danger')
- //id_navbar_feedback_message.hidden = false
+ //id_navbar_feedback_alert.alertContent = 'We have received your message. We will be in touch as soon as possible.'
+ //id_navbar_feedback_alert.classList.add('alert-success')
+ //id_navbar_feedback_alert.classList.remove('alert-danger')
+ //id_navbar_feedback_alert.hidden = false
id_navbar_feedback_icon.hidden = false
id_navbar_feedback_spinner.hidden = true
- id_navbar_feedback_message.hidden = true
+ id_navbar_feedback_alert.hidden = true
id_navbar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
$('#navbar-feedback-modal').modal('hide')
$('#navbar-message-modal').modal('show')
--- /dev/null
+let assert = require('assert')
+let jst_server = (await import('@ndcode/jst_server')).default
+let XDate = require('xdate')
+
+let arrays_equal =
+ (a, b) =>
+ a.length === b.length && a.every((value, index) => value === b[index])
+
+return async (env, head, body, scripts) => {
+ //let cart = await _require('/online_store/cart.jst')
+ let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
+ let fa_bars = await env.site.get_min_svg('/_svg/fa_bars.svg')
+ let fa_times_circle = await env.site.get_min_svg('/_svg/fa_times-circle.svg')
+ let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
+ let fa_unlock_alt = await env.site.get_min_svg('/_svg/fa_unlock-alt.svg')
+ let fa_search = await env.site.get_min_svg('/_svg/fa_search.svg')
+ let get_session = await _require('/_lib/get_session.jst')
+ //let icon_cart_small = await env.site.get_min_svg('/_svg/icon_cart_small.svg')
+ let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
+ //let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
+ let avatar_maker = await env.site.get_min_svg('/_svg/AvatarMaker.svg')
+ let page = await _require('/_lib/page.jst')
+
+ // initialize env.cart
+ //await cart(env)
+
+ // compute breadcrumbs from directories of the path
+ let component_names = env.parsed_url.pathname.split('/')
+ assert(component_names.length >= 2)
+ assert(component_names[0].length === 0)
+ assert(component_names[component_names.length - 1].length)
+ component_names = component_names.slice(1, -1)
+
+ let transaction = await env.site.database.Transaction()
+ let signed_in_as
+ let site_title, copyright
+ let component_titles // collects breadcrumb titles for current page
+ let menu_names, menu_titles // collects top level of menu for the sidebar
+ let feedback_draft
+ try {
+ let root = await transaction.get()
+
+ let session = await get_session(env, root)
+ signed_in_as = await session.get_json('signed_in_as')
+
+ let globals = await root.get('globals')
+ site_title = await globals.get_json('site_title')
+ copyright = await globals.get_json('copyright')
+
+ let navigation = await root.get('navigation')
+ if (navigation === undefined)
+ throw new jst_server.Problem(
+ 'Navigation error',
+ 'Please import the navigation tree into the database.',
+ 508
+ )
+
+ // this code is taken from get_navigation.jst and instrumented
+ let p = navigation
+ component_titles = [await p.get_json('title')] // Home
+ for (let i = 0; i < component_names.length; ++i) {
+ let children = await p.get('children')
+ p = await children.get(component_names[i])
+ if (navigation === undefined)
+ throw new jst_server.Problem(
+ 'Navigation error',
+ `Can't find the path "${
+ component_names.slice(0, i + 1).map(name => '/' + name).join('')
+ }" in the navigation tree.`,
+ 508
+ )
+ component_titles.push(await p.get_json('title'))
+ }
+
+ // similar to above but walks the top level laterally (not deeply)
+ menu_names = await navigation.get_json('menu')
+ let children = await navigation.get('children')
+ menu_titles = [await navigation.get_json('title')] // Home
+ for (let i = 0; i < menu_names.length; ++i) {
+ let child = await children.get(menu_names[i])
+ if (child === undefined)
+ throw new jst_server.Problem(
+ 'Navigation error',
+ `Can't find the path "/${menu_names[i]}" in the navigation tree.`
+ 508
+ )
+ menu_titles.push(await child.get('title'))
+ }
+
+ feedback_draft = await session.get_json('feedback_draft')
+ if (feedback_draft === undefined || env.now >= feedback_draft.expires)
+ feedback_draft = null
+ }
+ finally {
+ transaction.rollback()
+ }
+
+ // save breadcrumbs and their titles for breadcrumbs.jst
+ // note: component_titles.length === component_names.length + 1
+ // component_titles[0] corresponds to /, is 'Home' or similar
+ // component_titles[i] corresponds to component_names[i - 1], i >= 1
+ env.component_names = component_names
+ env.component_titles = component_titles
+
+ // note: menu_titles.length === menu_names.length + 1
+ // menu_titles[0] corresponds to /, is 'Home' or similar
+ // menu_titles[i] corresponds to menu_names[i - 1], i >= 1
+ // (sidebar has Home appearing at same level as its immediate children)
+
+ await page(
+ env,
+ // head
+ async _out => {
+ title {
+ `${site_title}: ${
+ component_titles[
+ component_names.length >= 2 ? 1 : component_names.length
+ ]
+ }`
+ }
+
+ await head(_out)
+ },
+ // body
+ async _out => {
+ // extract top-level directory name
+ assert(env.parsed_url.pathname.slice(0, 1) === '/')
+ let index = env.parsed_url.pathname.indexOf('/', 1)
+ let dir = index === -1 ? '' : env.parsed_url.pathname.slice(1, index)
+
+ div.container-fluid {
+ div.row {
+ div.col-md.sidebar-outer.sidebar-outer-collapsed#sidebar-outer {
+ nav.sidebar-inner.d-flex.flex-column#sidebar-inner {
+ div.mb-4 {
+ div(style="width: 128px; height: 128px;") {
+ _out.push(avatar_maker)
+ }
+ b.h1 {
+ `${site_title}`
+ }
+ }
+
+ div.mb-2 {
+ span#sidebar-signed-in-status {
+ if (signed_in_as !== undefined)
+ 'Signed in.'
+ else
+ 'Signed out.'
+ }
+ ' '
+ if (signed_in_as !== undefined)
+ a#sidebar-sign-in(href="#" hidden) {'Sign in'}
+ else
+ a#sidebar-sign-in(href="#") {'Sign in'}
+ ' '
+ if (signed_in_as !== undefined)
+ a#sidebar-sign-up(href="/my_account/sign_up/index.html" hidden) {'Sign up'}
+ else
+ a#sidebar-sign-up(href="/my_account/sign_up/index.html") {'Sign up'}
+ ' '
+ if (signed_in_as !== undefined)
+ a#sidebar-sign-out(href="#") {'Sign out'}
+ else
+ a#sidebar-sign-out(href="#" hidden) {'Sign out'}
+ }
+
+ form.mb-4(action="/search/index.html") {
+ div.input-group {
+ input.form-control(name="query" type="text" placeholder="Search" aria-describedby="search-button") {}
+ div.input-group-append {
+ button.btn.btn-outline-secondary#sidebar-search-button(type="submit") {
+ div.icon24-outer {
+ div.icon24-inner {_out.push(fa_search)}
+ }
+ }
+ }
+ }
+ }
+
+ // the active entry in the sidebar bar is based on which top-level
+ // page we are under, even if we are not directly on that page
+ // but one of its children, this may be unexpected as the active
+ // entry does not highlight on hover, but you can still click it;
+ // we determine here the path to the corresponding top-level page
+ let component_prefix = component_names.slice(0, 1)
+
+ for (let i = 0; i < menu_titles.length; ++i) {
+ // construct path to the top-level page about to be described
+ let menu_prefix =
+ i === 0 ? [] : [menu_names[i - 1]]
+ let menu_prefix_path =
+ menu_prefix.map(name => '/' + name).join('') + '/index.html'
+
+ if (arrays_equal(menu_prefix, component_prefix))
+ div.nav-item.active {
+ a.nav-link.nav-link2.grid-gutter-background(href=menu_prefix_path) {
+ `${menu_titles[i]}`
+ span.sr-only {' (current)'}
+ }
+ }
+ else
+ div.nav-item {
+ a.nav-link.nav-link2.grid-gutter-background(href=menu_prefix_path) {
+ `${menu_titles[i]}`
+ }
+ }
+ }
+ div.nav-item.mt-auto {
+ a.nav-link.nav-link2.grid-gutter-background#sidebar-give-feedback(href="#") {'Give feedback'}
+ }
+ }
+ }
+
+ div.col-md.sidebar-content {
+ // the breadcrumbs have already been determined by sidebar.jst, as
+ // the HTML title is similar to the breadcrumbs (but without links)
+ let component_names = env.component_names
+ let component_titles = env.component_titles
+
+ // present component_titles as breadcrumbs, except last one as text
+ h2.page-header.grid-gutter-background.'py-2'.mb-0 {
+ button.btn.btn-outline-secondary.sidebar-toggle.mr-3#sidebar-toggle {
+ div.icon24-outer(style="top: -1px;") {
+ div.icon24-inner {_out.push(fa_bars)}
+ }
+ span.sr-only {'Navbar toggle'}
+ }
+ for (let i = 0; i < component_names.length; ++i) {
+ a.h4(
+ href=
+ `${
+ component_names.slice(0, i).map(name => '/' + name).join('')
+ }/index.html`
+ ) {`${component_titles[i]}`}
+ ' '
+ span.h5 {'>'}
+ ' '
+ }
+ `${component_titles[component_names.length]}`
+ }
+
+ await body(_out)
+
+ footer.page-footer.grid-gutter-background.py-5 {
+ a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
+ img(alt="Creative Commons License" style="border-width:0;" src="/images/by-sa_3.0_88x31.png") {}
+ }
+ p {
+ 'This website is '
+ a(href="https://git.ndcode.org/public/nick_site.git") {
+ 'open source'
+ }
+ ' and licensed under a '
+ a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
+ 'Creative Commons Attribution-ShareAlike 3.0 Unported License'
+ }
+ '.'
+ }
+
+ p.mb-0 {`Copyright © ${new XDate(env.now).getUTCFullYear()} ${copyright}.`}
+ }
+ }
+ div.col-md.sidebar-dummy {}
+ }
+ }
+
+ // hidden part
+ div.modal#sidebar-sign-in-modal(tabindex="-1") {
+ div.modal-dialog {
+ div.modal-content {
+ div.modal-header {
+ span.h4.modal-title {'Sign in'}
+ }
+ div.modal-body {
+ form.mb-2#sidebar-sign-in-form {
+ div.row {
+ div.col-md-12 {
+ div.form-group {
+ label.form-label(for="sidebar-sign-in-email") {'Email'}
+ input.form-control#sidebar-sign-in-email(type="email" required maxlength=256) {}
+ div.invalid-feedback {'Please enter your account\'s email address.'}
+ }
+ }
+ }
+ div.row {
+ div.col-md-12 {
+ div.form-group {
+ label.form-label(for="sidebar-sign-in-password") {'Password'}
+ input.form-control#sidebar-sign-in-password(type="password" required minlength=8 maxlength=256) {}
+ div.invalid-feedback {'Please enter at least 8 characters.'}
+ }
+ }
+ }
+ }
+
+ p {
+ 'No account yet? '
+ a(href="/my_account/sign_up/index.html") {'Sign up'}
+ }
+
+ p.mb-0 {
+ 'Forgot password? '
+ a(href="/my_account/password_reset/index.html") {'Password reset'}
+ }
+
+ div.alert.alert-danger.'mt-3'.mb-0#sidebar-sign-in-alert(hidden) {}
+ }
+ div.modal-footer {
+ button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+ div.icon24-outer.mr-2 {
+ div.icon24-inner {_out.push(fa_arrow_circle_left)}
+ }
+ 'Back'
+ }
+ button.btn.btn-primary#sidebar-sign-in-sign-in(type="button") {
+ div.icon24-outer.mr-2#sidebar-sign-in-icon {
+ div.icon24-inner {_out.push(fa_unlock_alt)}
+ }
+ //div.icon24-outer.mr-2#sidebar-sign-in-tick(hidden) {
+ // div.icon24-inner {_out.push(icon_tick)}
+ //}
+ div.icon24-outer.mr-2#sidebar-sign-in-cross(hidden) {
+ div.icon24-inner {_out.push(icon_cross)}
+ }
+ div.icon24-outer.mr-2#sidebar-sign-in-spinner(hidden) {
+ div.icon24-inner {
+ div.spinner-border.spinner-border-sm(role="status") {}
+ }
+ }
+ 'Sign in'
+ }
+ }
+ }
+ }
+ }
+
+ div.modal#sidebar-feedback-modal(tabindex="-1") {
+ div.modal-dialog {
+ div.modal-content {
+ div.modal-header {
+ span.h4.modal-title {'Give feedback'}
+ }
+ div.modal-body {
+ p {
+ 'Did you notice something not quite right, or just want to share your impression of this page?'
+ }
+
+ form#sidebar-feedback-form {
+ div.row {
+ div.col-md-12 {
+ div.form-group {
+ label.form-label(for="sidebar-feedback-message") {'Message'}
+ textarea.form-control#sidebar-feedback-message(placeholder="I noticed that..." required rows=4 maxlength=65536) {
+ if (feedback_draft)
+ `${feedback_draft.message}`
+ }
+ div.invalid-feedback {'Please let us have your thoughts.'}
+ }
+ }
+ }
+ }
+
+ div.alert.alert-danger.'mt-3'.mb-0#sidebar-feedback-alert(hidden) {}
+ }
+ div.modal-footer {
+ button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+ div.icon24-outer.mr-2 {
+ div.icon24-inner {_out.push(fa_arrow_circle_left)}
+ }
+ 'Back'
+ }
+ if (feedback_draft)
+ button.btn.btn-primary#sidebar-feedback-send-message(type="button") {
+ div.icon24-outer.mr-2#sidebar-feedback-icon {
+ div.icon24-inner {_out.push(fa_envelope)}
+ }
+ //div.icon24-outer.mr-2#sidebar-feedback-tick(hidden) {
+ // div.icon24-inner {_out.push(icon_tick)}
+ //}
+ div.icon24-outer.mr-2#sidebar-feedback-cross(hidden) {
+ div.icon24-inner {_out.push(icon_cross)}
+ }
+ div.icon24-outer.mr-2#sidebar-feedback-spinner(hidden) {
+ div.icon24-inner {
+ div.spinner-border.spinner-border-sm(role="status") {}
+ }
+ }
+ 'Send message'
+ }
+ else
+ button.btn.btn-primary#sidebar-feedback-send-message(type="button" disabled) {
+ div.icon24-outer.mr-2#sidebar-feedback-icon {
+ div.icon24-inner {_out.push(fa_envelope)}
+ }
+ //div.icon24-outer.mr-2#sidebar-feedback-tick(hidden) {
+ // div.icon24-inner {_out.push(icon_tick)}
+ //}
+ div.icon24-outer.mr-2#sidebar-feedback-cross(hidden) {
+ div.icon24-inner {_out.push(icon_cross)}
+ }
+ div.icon24-outer.mr-2#sidebar-feedback-spinner(hidden) {
+ div.icon24-inner {
+ div.spinner-border.spinner-border-sm(role="status") {}
+ }
+ }
+ 'Send message'
+ }
+ }
+ }
+ }
+ }
+
+ div.modal#sidebar-message-modal(tabindex="-1") {
+ div.modal-dialog {
+ div.modal-content {
+ div.modal-header {
+ span.h4.modal-title {'Message'}
+ }
+ div.modal-body#sidebar-message-modal-message {
+ }
+ div.modal-footer {
+ button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+ div.icon24-outer.mr-2 {
+ div.icon24-inner {_out.push(fa_times_circle)}
+ }
+ 'Close'
+ }
+ }
+ }
+ }
+ }
+ },
+ // scripts
+ async _out => {
+ script(src="/js/utils.js") {}
+
+ script {
+ // this function can be overridden in a further script
+ function sign_in_out(status) {
+ return false
+ }
+
+ document.addEventListener(
+ 'DOMContentLoaded',
+ () => {
+ let id_sidebar_feedback_alert = document.getElementById('sidebar-feedback-alert')
+ let id_sidebar_feedback_cross = document.getElementById('sidebar-feedback-cross')
+ let id_sidebar_feedback_form = document.getElementById('sidebar-feedback-form')
+ let id_sidebar_feedback_icon = document.getElementById('sidebar-feedback-icon')
+ let id_sidebar_feedback_message = document.getElementById('sidebar-feedback-message')
+ let id_sidebar_feedback_modal = document.getElementById('sidebar-feedback-modal')
+ let id_sidebar_feedback_send_message = document.getElementById('sidebar-feedback-send-message')
+ let id_sidebar_feedback_spinner = document.getElementById('sidebar-feedback-spinner')
+ let id_sidebar_feedback_tick = document.getElementById('sidebar-feedback-tick')
+ let id_sidebar_give_feedback = document.getElementById('sidebar-give-feedback')
+ let id_sidebar_inner = document.getElementById('sidebar-inner')
+ let id_sidebar_message_modal = document.getElementById('sidebar-message-modal')
+ let id_sidebar_message_modal_message = document.getElementById('sidebar-message-modal-message')
+ let id_sidebar_outer = document.getElementById('sidebar-outer')
+ let id_sidebar_search_button = document.getElementById('sidebar-search-button')
+ let id_sidebar_sign_in = document.getElementById('sidebar-sign-in')
+ let id_sidebar_sign_in_alert = document.getElementById('sidebar-sign-in-alert')
+ let id_sidebar_sign_in_cross = document.getElementById('sidebar-sign-in-cross')
+ let id_sidebar_sign_in_email = document.getElementById('sidebar-sign-in-email')
+ let id_sidebar_sign_in_form = document.getElementById('sidebar-sign-in-form')
+ let id_sidebar_sign_in_icon = document.getElementById('sidebar-sign-in-icon')
+ let id_sidebar_sign_in_modal = document.getElementById('sidebar-sign-in-modal')
+ let id_sidebar_sign_in_password = document.getElementById('sidebar-sign-in-password')
+ let id_sidebar_sign_in_sign_in = document.getElementById('sidebar-sign-in-sign-in')
+ let id_sidebar_sign_in_spinner = document.getElementById('sidebar-sign-in-spinner')
+ let id_sidebar_sign_in_tick = document.getElementById('sidebar-sign-in-tick')
+ let id_sidebar_sign_out = document.getElementById('sidebar-sign-out')
+ let id_sidebar_sign_up = document.getElementById('sidebar-sign-up')
+ let id_sidebar_signed_in_status = document.getElementById('sidebar-signed-in-status')
+ let id_sidebar_toggle = document.getElementById('sidebar-toggle')
+
+ // sign in form
+ id_sidebar_sign_in.addEventListener(
+ 'click',
+ () => {
+ id_sidebar_sign_in_email.value = ''
+ id_sidebar_sign_in_password.value = ''
+ id_sidebar_sign_in_sign_in.disabled = true
+ $('#sidebar-sign-in-modal').modal('show')
+ }
+ )
+
+ $('#sidebar-sign-in-modal').on(
+ 'shown.bs.modal',
+ () => {id_sidebar_sign_in_email.focus()}
+ )
+
+ let sign_in_edited = () => {
+ id_sidebar_sign_in_sign_in.disabled =
+ id_sidebar_sign_in_email.value.length === 0 &&
+ id_sidebar_sign_in_password.value.length === 0
+ id_sidebar_sign_in_icon.hidden = false
+ //id_sidebar_sign_in_tick.hidden = true
+ id_sidebar_sign_in_cross.hidden = true
+ id_sidebar_sign_in_spinner.hidden = true
+ id_sidebar_sign_in_alert.hidden = true
+ }
+
+ id_sidebar_sign_in_email.addEventListener(
+ 'input',
+ sign_in_edited
+ )
+ id_sidebar_sign_in_password.addEventListener(
+ 'input',
+ sign_in_edited
+ )
+
+ id_sidebar_sign_in_sign_in.addEventListener(
+ 'click',
+ async () => {
+ id_sidebar_sign_in_icon.hidden = false
+ //id_sidebar_sign_in_tick.hidden = true
+ id_sidebar_sign_in_cross.hidden = true
+ id_sidebar_sign_in_spinner.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_sidebar_sign_in_alert.hidden = true
+
+ if (!id_sidebar_sign_in_form.checkValidity()) {
+ id_sidebar_sign_in_form.classList.add('was-validated');
+
+ id_sidebar_sign_in_icon.hidden = true
+ id_sidebar_sign_in_cross.hidden = false
+ return
+ }
+ id_sidebar_sign_in_form.classList.remove('was-validated');
+
+ let email = id_sidebar_sign_in_email.value.slice(0, 256).toLowerCase()
+
+ id_sidebar_sign_in_icon.hidden = true
+ id_sidebar_sign_in_spinner.hidden = false
+ try {
+ await api_call(
+ '/api/account/sign_in.json',
+ email,
+ id_sidebar_sign_in_password.value.slice(0, 256)
+ )
+ }
+ catch (error) {
+ let problem = Problem.from(error)
+
+ if (problem.title === 'Email not yet verified') {
+ location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
+ return
+ }
+
+ id_sidebar_sign_in_cross.hidden = false
+ id_sidebar_sign_in_spinner.hidden = true
+
+ id_sidebar_sign_in_alert.textContent = problem.detail
+ //id_sidebar_sign_in_alert.classList.remove('alert-success')
+ //id_sidebar_sign_in_alert.classList.add('alert-danger')
+ id_sidebar_sign_in_alert.hidden = false
+ return
+ }
+ //id_sidebar_sign_in_tick.hidden = false
+ //id_sidebar_sign_in_spinner.hidden = true
+ //id_sidebar_sign_in_alert.textContent = `You are now signed in as "${email}".`
+ //id_sidebar_sign_in_alert.classList.add('alert-success')
+ //id_sidebar_sign_in_alert.classList.remove('alert-danger')
+ //id_sidebar_sign_in_alert.hidden = false
+
+ if (sign_in_out(true))
+ // if location has been changed, leave the spinner and do
+ // not show status/dialog, as it causes an annoying flicker
+ return
+
+ id_sidebar_signed_in_status.textContent = 'Signed in.'
+ id_sidebar_sign_in.hidden = true
+ id_sidebar_sign_up.hidden = true
+ id_sidebar_sign_out.hidden = false
+
+ id_sidebar_sign_in_icon.hidden = false
+ id_sidebar_sign_in_spinner.hidden = true
+ id_sidebar_sign_in_alert.hidden = true
+ id_sidebar_message_modal_message.textContent = `You are now signed in as "${email}".`
+ $('#sidebar-sign-in-modal').modal('hide')
+ $('#sidebar-message-modal').modal('show')
+ }
+ )
+
+ // sign out button
+ id_sidebar_sign_out.addEventListener(
+ 'click',
+ async () => {
+ try {
+ await api_call(
+ '/api/account/sign_out.json'
+ )
+ }
+ catch (error) {
+ let problem = Problem.from(error)
+
+ id_sidebar_message_modal_message.textContent = problem.detail
+ $('#sidebar-sign-in-modal').modal('hide')
+ $('#sidebar-message-modal').modal('show')
+ return
+ }
+
+ if (sign_in_out(false))
+ // if location has been changed, leave the spinner and do
+ // not show status/dialog, as it causes an annoying flicker
+ return
+
+ id_sidebar_signed_in_status.textContent = 'Signed out.'
+ id_sidebar_sign_in.hidden = false
+ id_sidebar_sign_up.hidden = false
+ id_sidebar_sign_out.hidden = true
+
+ id_sidebar_message_modal_message.textContent = `You are now signed out.`
+ $('#sidebar-sign-in-modal').modal('hide')
+ $('#sidebar-message-modal').modal('show')
+ }
+ )
+
+ // feedback form
+ id_sidebar_give_feedback.addEventListener(
+ 'click',
+ () => {
+ // hack to move cursor to end of textarea
+ let temp = id_sidebar_feedback_message.value
+ id_sidebar_feedback_message.value = ''
+ id_sidebar_feedback_message.value = temp
+
+ $('#sidebar-feedback-modal').modal('show')
+ return false
+ }
+ )
+
+ $('#sidebar-feedback-modal').on(
+ 'shown.bs.modal',
+ () => {id_sidebar_feedback_message.focus()}
+ )
+
+ let feedback_input_semaphore = new BinarySemaphore(false)
+ ;(
+ async () => {
+ while (true) {
+ await feedback_input_semaphore.acquire()
+ await new Promise(resolve => setTimeout(resolve, 3000))
+ feedback_input_semaphore.try_acquire()
+ await api_call(
+ '/api/feedback/set_draft.json',
+ id_sidebar_feedback_message.value.length === 0 ?
+ null :
+ {
+ message: id_sidebar_feedback_message.value.slice(0, 65536)
+ }
+ )
+ }
+ }
+ )() // ignore returned promise (start thread)
+
+ let feedback_edited = () => {
+ feedback_input_semaphore.release()
+
+ id_sidebar_feedback_send_message.disabled =
+ id_sidebar_feedback_message.value.length === 0
+ id_sidebar_feedback_icon.hidden = false
+ //id_sidebar_feedback_tick.hidden = true
+ id_sidebar_feedback_cross.hidden = true
+ id_sidebar_feedback_spinner.hidden = true
+ id_sidebar_feedback_alert.hidden = true
+ }
+
+ id_sidebar_feedback_message.addEventListener(
+ 'input',
+ feedback_edited
+ )
+
+ id_sidebar_feedback_send_message.addEventListener(
+ 'click',
+ async () => {
+ id_sidebar_feedback_icon.hidden = false
+ //id_sidebar_feedback_tick.hidden = true
+ id_sidebar_feedback_cross.hidden = true
+ id_sidebar_feedback_spinner.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_sidebar_feedback_alert.hidden = true
+
+ if (!id_sidebar_feedback_form.checkValidity()) {
+ id_sidebar_feedback_form.classList.add('was-validated');
+
+ id_sidebar_feedback_icon.hidden = true
+ id_sidebar_feedback_cross.hidden = false
+ return
+ }
+ id_sidebar_feedback_form.classList.remove('was-validated');
+
+ id_sidebar_feedback_icon.hidden = true
+ id_sidebar_feedback_spinner.hidden = false
+ try {
+ await api_call(
+ '/api/feedback/send_message.json',
+ location.href,
+ id_sidebar_feedback_message.value.slice(0, 65536)
+ )
+ }
+ catch (error) {
+ let problem = Problem.from(error)
+
+ id_sidebar_feedback_cross.hidden = false
+ id_sidebar_feedback_spinner.hidden = true
+
+ id_sidebar_feedback_alert.textContent = problem.detail
+ //id_sidebar_feedback_alert.classList.remove('alert-success')
+ //id_sidebar_feedback_alert.classList.add('alert-danger')
+ id_sidebar_feedback_alert.hidden = false
+ return
+ }
+ //id_sidebar_feedback_tick.hidden = false
+ //id_sidebar_feedback_spinner.hidden = true
+ //id_sidebar_feedback_alert.alertContent = 'We have received your message. We will be in touch as soon as possible.'
+ //id_sidebar_feedback_alert.classList.add('alert-success')
+ //id_sidebar_feedback_alert.classList.remove('alert-danger')
+ //id_sidebar_feedback_alert.hidden = false
+
+ id_sidebar_feedback_icon.hidden = false
+ id_sidebar_feedback_spinner.hidden = true
+ id_sidebar_feedback_alert.hidden = true
+ id_sidebar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
+ $('#sidebar-feedback-modal').modal('hide')
+ $('#sidebar-message-modal').modal('show')
+ }
+ )
+
+ // sidebar
+ let sidebar_outer_computed_style = window.getComputedStyle(
+ id_sidebar_outer
+ )
+ let sidebar_toggle_computed_style = window.getComputedStyle(
+ id_sidebar_toggle
+ )
+ let sidebar_is_collapsed =
+ () =>
+ sidebar_toggle_computed_style.display !== 'none' &&
+ id_sidebar_outer.classList.contains(
+ 'sidebar-outer-collapsed'
+ )
+ let sidebar_collapse_update = () => {
+ if (sidebar_outer_computed_style.position === 'sticky') { // md and up
+ id_sidebar_outer.style.flexBasis =
+ sidebar_is_collapsed() ?
+ '0px' :
+ `${id_sidebar_inner.clientWidth}px`
+ id_sidebar_outer.style.removeProperty('height')
+ }
+ else {
+ id_sidebar_outer.style.height =
+ sidebar_is_collapsed() ?
+ '0px' :
+ `${id_sidebar_inner.clientHeight}px`
+ id_sidebar_outer.style.removeProperty('flex-basis')
+ }
+ }
+ window.addEventListener('resize', sidebar_collapse_update)
+ sidebar_collapse_update()
+
+ id_sidebar_outer.addEventListener(
+ 'transitionend',
+ () => {
+ // transitions are only allowed after clicking collapse button,
+ // otherwise they can be triggered by media queries on resize
+ id_sidebar_outer.style.removeProperty('transition-property')
+ }
+ )
+
+ id_sidebar_toggle.addEventListener(
+ 'click',
+ () => {
+ if (
+ id_sidebar_outer.classList.contains(
+ 'sidebar-outer-collapsed'
+ )
+ )
+ id_sidebar_outer.classList.remove(
+ 'sidebar-outer-collapsed'
+ )
+ else
+ id_sidebar_outer.classList.add(
+ 'sidebar-outer-collapsed'
+ )
+ id_sidebar_outer.style.transitionProperty =
+ sidebar_outer_computed_style.position === 'sticky' ?
+ 'flex-basis' : // md and up
+ 'height'
+ sidebar_collapse_update()
+ }
+ )
+ }
+ )
+ }
+
+ await scripts(_out)
+ }
+ )
+}
let children = await p.get('children')
let menu = await p.get_json('menu')
- ul.nav.flex-column {
+ nav {
for (let i = 0; i < menu.length; ++i) {
let name = menu[i]
let q = await children.get(name)
+ let images = await q.get_json('images')
- li.nav-item {
- a.nav-link(href=`${name}/index.html`) {
- table.icon-and-text {
- tr {
- td {
- img(
- src=`${name}/${await q.get_json('thumbnail') || 'thumbnail.jpg'}`
- ) {}
- }
- td {
- span.h2{
- `${await q.get_json('title')}`
- }
- br {}
- span.h4{
- `${await q.get_json('description')}—by ${await q.get_json('author')}`
- }
- }
+ // we can use higher resolution images when lower not available
+ while (images.length < 3)
+ images.push(images[images.length - 1])
+
+ // note: make the row a link to show hover background at edges
+ // (similar to the grid-gutter-background class I use elsewhere)
+ // note: position: relative sets the target for stretched link
+ div.row.flex-nowrap.align-items-center.nav-link-outer.position-relative {
+ div.col.col-icon256 {
+ img.img-fluid.py-3(src=`${name}/${images[2]}`) {}
+ }
+ div.col.col-text {
+ a.nav-link-inner.stretched-link(href=`${name}/index.html`) {
+ span.h2{
+ `${await q.get_json('title')}`
+ }
+ br {}
+ span.h4{
+ `${await q.get_json('description')}—by ${await q.get_json('author')}`
}
}
}
async _out => {
await breadcrumbs(env, _out)
- p {'Do you require more information, or assistance with integrating the projects on this site? We’d love to hear from you.'}
+ p/*.mt-3*/ {'Do you require more information, or assistance with integrating the projects on this site? We’d love to hear from you.'}
form#form {
div.row {
div.row {
div.col-md-12 {
div.form-group {
- label.form-label(for="message1") {'Message *'}
- textarea.form-control#message1(placeholder="I would like to..." required rows=6 maxlength=65536) {
+ label.form-label(for="message") {'Message *'}
+ textarea.form-control#message(placeholder="I am interested in..." required rows=6 maxlength=65536) {
if (contact_draft)
`${contact_draft.message}`
}
}
if (contact_draft !== null)
- button.btn.btn-success#send-enquiry(type="button") {
+ button.btn.btn-success.mb-3#send-enquiry(type="button") {
div.icon24-outer.mr-2#icon {
div.icon24-inner {_out.push(fa_envelope)}
}
'Send enquiry'
}
else
- button.btn.btn-success#send-enquiry(type="button" disabled) {
+ button.btn.btn-success.mb-3#send-enquiry(type="button" disabled) {
div.icon24-outer.mr-2#icon {
div.icon24-inner {_out.push(fa_envelope)}
}
'Send enquiry'
}
- p.'mt-3'.mb-0#message(hidden) {}
+ div.alert#alert(hidden) {}
- p.text-muted.mt-3 {'* These fields are required.'}
+ p.text-muted {'* These fields are required.'}
},
// scripts
async _out => {
document.addEventListener(
'DOMContentLoaded',
() => {
+ let id_alert = document.getElementById('alert')
let id_company = document.getElementById('company')
let id_cross = document.getElementById('cross')
let id_email = document.getElementById('email')
let id_given_names = document.getElementById('given-names')
let id_icon = document.getElementById('icon')
let id_message = document.getElementById('message')
- let id_message1 = document.getElementById('message1')
let id_send_enquiry = document.getElementById('send-enquiry')
let id_spinner = document.getElementById('spinner')
let id_tick = document.getElementById('tick')
id_family_name.value.length === 0 &&
id_company.value.length === 0 &&
id_email.value.length === 0 &&
- id_message1.value.length === 0 ?
+ id_message.value.length === 0 ?
null :
{
given_names: id_given_names.value.slice(0, 256),
family_name: id_family_name.value.slice(0, 256),
company: id_company.value.slice(0, 256),
email: id_email.value.slice(0, 256).toLowerCase(),
- message: id_message1.value.slice(0, 65536)
+ message: id_message.value.slice(0, 65536)
}
)
}
id_family_name.value.length === 0 &&
id_company.value.length === 0 &&
id_email.value.length === 0 &&
- id_message1.value.length === 0
+ id_message.value.length === 0
id_icon.hidden = false
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- id_message.hidden = true
+ id_alert.hidden = true
}
id_given_names.addEventListener('input', edited)
id_family_name.addEventListener('input', edited)
id_company.addEventListener('input', edited)
id_email.addEventListener('input', edited)
- id_message1.addEventListener('input', edited)
+ id_message.addEventListener('input', edited)
id_send_enquiry.addEventListener(
'click',
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_alert.hidden = true
if (!id_form.checkValidity()) {
id_form.classList.add('was-validated');
family_name: id_family_name.value.slice(0, 256),
company: id_company.value.slice(0, 256),
email: id_email.value.slice(0, 256).toLowerCase(),
- message: id_message1.value.slice(0, 65536)
+ message: id_message.value.slice(0, 65536)
}
)
}
id_cross.hidden = false
id_spinner.hidden = true
- id_message.textContent = problem.detail
- //id_message.classList.remove('text-success')
- id_message.classList.add('text-danger')
- id_message.hidden = false
+ id_alert.textContent = problem.detail
+ id_alert.classList.remove('alert-success')
+ id_alert.classList.add('alert-danger')
+ id_alert.hidden = false
return
}
id_tick.hidden = false
id_spinner.hidden = true
- id_message.textContent = 'We have received your enquiry. We will be in touch as soon as possible.'
- //id_message.classList.add('text-success')
- id_message.classList.remove('text-danger')
- id_message.hidden = false
+ id_alert.textContent = 'We have received your enquiry. We will be in touch as soon as possible.'
+ id_alert.classList.add('alert-success')
+ id_alert.classList.remove('alert-danger')
+ id_alert.hidden = false
}
)
}
}
}
-// place a container div around entire page, and then use this
-// to make the background on navbar or footer appear full-width
-.extend-background {
+// use within a container to extend the background to left/right
+.container-background {
margin-left: calc(-.5 * (100vw - 100%));
margin-right: calc(-.5 * (100vw - 100%));
padding-left: calc(.5 * (100vw - 100%));
padding-right: .5 * $grid-gutter-width;
}
-.sidebar {
- position: sticky;
- top: 0;
- max-height: 100vh;
- overflow-y: auto;
+// similar to bootstrap's navbar class, but appears to the side
+// responsive in a fairly sophisticated way, with 5 different modes:
+// >= xs: appears above content, toggleable, animates vertically
+// >= md: appears to left of content, toggleable, animates horizontally
+// >= xl: appears to left of content, always visible
+// >= 2xl: same as xl but has a right margin half the width of the sidebar
+// >= 3xl: same as xl but has a right margin the same width as the sidebar
+// (the last way gives a balanced look, as the content is along the middle)
+// note: flex item cannot be smaller than its contents with overflow visible,
+// so to be safe (when screen so small that a word cannot fit) make it hidden
+.sidebar-outer {
+ // note: the following properties are set via javascript using inline style:
+ // height: 0px if hidden and animation is vertical, otherwise content height
+ // flex-basis: 0px if hidden and animation is horizontal, otherwise 350px
+ // transition-property: flex-basis / height during animation, otherwise none
+ flex-grow: 0;
+ flex-shrink: 0;
+ padding-left: 0;
+ padding-right: 0;
+ overflow: hidden;
+ @include media-breakpoint-up(md) {
+ position: sticky;
+ top: 0;
+ max-height: 100vh;
+ overflow-y: auto;
+ }
+ transition: none .35s ease;
background-color: $gray-200;
}
+.sidebar-inner {
+ padding-left: $grid-gutter-width * .5;
+ padding-right: $grid-gutter-width * .5;
+ @include media-breakpoint-up(md) {
+ width: 350px;
+ min-height: 100vh;
+ }
+}
+.sidebar-toggle {
+ @include media-breakpoint-up(xl) {
+ display: none;
+ }
+}
+.sidebar-content {
+ overflow: hidden;
+ // the following gives an effect more like .container, as the text wrap
+ // becomes predictable after a certain screen size, but text is biased
+ // to the left on larger screens, I prefer to have it wider and centred
+ //max-width: 1280px;
+}
+.sidebar-dummy {
+ flex: 0 0 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ overflow: hidden;
+ @include media-breakpoint-up(2xl) {
+ flex-basis: 175px;
+ }
+ @include media-breakpoint-up(3xl) {
+ flex-basis: 350px;
+ }
+}
+
+// used in blog posts for a responsive N-column layout
+.rowN {
+ display: flex;
+ flex-wrap: wrap;
+ //margin-left: $grid-gutter-width * -.5;
+ //margin-right: $grid-gutter-width * -.5;
+}
+.rowN-header {
+ display: none;
+ @include media-breakpoint-up(2xl) {
+ display: flex;
+ }
+}
+.col7 {
+ padding-left: $grid-gutter-width * .5;
+ padding-right: $grid-gutter-width * .5;
+ flex: 1 0 100%;
+ @include media-breakpoint-up(2xl) {
+ flex-basis: 14.285714%;
+ max-width: 14.285714%;
+ }
+ border: 2px solid white;
+ hyphens: auto;
+}
+.col7-2 {
+ padding-left: $grid-gutter-width * .5;
+ padding-right: $grid-gutter-width * .5;
+ flex: 1 0 100%;
+ @include media-breakpoint-up(2xl) {
+ flex-basis: 28.571429%;
+ max-width: 28.571429%;
+ }
+ border: 2px solid white;
+ hyphens: auto;
+}
+.colN-data {
+ background-color: theme-color-level("primary", $alert-bg-level);
+}
+.colN-header {
+ background-color: theme-color-level("success", $alert-bg-level);
+}
+
+// use after .img-fluid
+.img-fluid-1-3 {
+ max-width: 33.333333%
+}
+.img-fluid-2-3 {
+ max-width: 66.666667%
+}
// needed for svg icons inside buttons, card headers, etc
// creates an inline element with correct width but no height
font-family: $font-family-serif;
}
-// apply this to table to get an icon with vertically centred text next to it
-.icon-and-text {
- vertical-align: middle;
- td {
- padding: .25em .5em;
- }
+// use this with bootstrap's grid system to make an icon with text beside it
+// the icon can take up to 1/2 of the width, but not more than its N-px width
+// note: flex item cannot be smaller than its contents with overflow visible,
+// so to be safe (when screen so small that a word cannot fit) make it hidden
+// note: col has position: relative, and I believe this is so that stretched
+// links will fill the column, but we want them to fill the row in this case
+.col-icon128 {
+ flex: 0 1 calc(128px + $grid-gutter-width);
+ overflow: hidden;
+ position: static;
}
-
-.search-results li {
- margin-bottom: .5em;
+.col-icon256 {
+ flex: 0 1 calc(256px + $grid-gutter-width);
+ overflow: hidden;
+ position: static;
+}
+.col-text {
+ flex: 1 0 50%;
+ overflow: hidden;
+ position: static;
}
.page-header {
}
}
+// Nick (for stretched links)
+.nav-link-inner {
+ display: block;
+ padding: $nav-link-padding-y $nav-link-padding-x;
+ text-decoration: if($link-decoration == none, null, none);
+}
+.nav-link-outer {
+ @include hover-focus() {
+ text-decoration: none;
+ background: $nav-link-hover-bg; // Nick
+ }
+
+ // Disabled state lightens text
+ &.disabled {
+ color: $nav-link-disabled-color;
+ pointer-events: none;
+ cursor: default;
+ }
+}
+
//
// Tabs
//
b,
strong {
- font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari
+ font-weight: $font-weight-bold/*er*/; // Add the correct font weight in Chrome, Edge, and Safari
}
small {
md: 768px,
lg: 992px,
xl: 1200px,
- //xxl: 1500px // Nick (for sidebar)
+ // Nick (for sidebar)
+ //2xl: 1500px,
+ //3xl: 1800px
) !default;
@include _assert-ascending($grid-breakpoints, "$grid-breakpoints");
md: 720px,
lg: 960px,
xl: 1140px,
- //xxl: 1420px // Nick (for sidebar)
+ // Nick (for sidebar)
+ //2xl: 1420px,
+ //3xl: 1700px
) !default;
@include _assert-ascending($container-max-widths, "$container-max-widths");
+++ /dev/null
-/*!
- * IE10 viewport hack for Surface/desktop Windows 8 bug
- * Copyright 2014-2015 Twitter, Inc.
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
- */
-
-/*
- * See the Getting Started docs for more information:
- * http://getbootstrap.com/getting-started/#support-ie10-width
- */
-@-ms-viewport { width: device-width; }
-@-o-viewport { width: device-width; }
-@viewport { width: device-width; }
+++ /dev/null
-//@font-face {
-// font-family: 'Open Sans';
-// font-weight: normal;
-// font-style: normal;
-// src: url('../jsdoc/fonts/OpenSans-Regular-webfont.eot');
-// src:
-// local('Open Sans'),
-// local('OpenSans'),
-// url('../jsdoc/fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'),
-// url('../jsdoc/fonts/OpenSans-Regular-webfont.woff') format('woff'),
-// url('../jsdoc/fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg');
-//}
-//
-//@font-face {
-// font-family: 'Open Sans Light';
-// font-weight: normal;
-// font-style: normal;
-// src: url('../jsdoc/fonts/OpenSans-Light-webfont.eot');
-// src:
-// local('Open Sans Light'),
-// local('OpenSans Light'),
-// url('../jsdoc/fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'),
-// url('../jsdoc/fonts/OpenSans-Light-webfont.woff') format('woff'),
-// url('../jsdoc/fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg');
-//}
-
-.jsdoc {
-
-//html
-//{
-// overflow: auto;
-// background-color: #fff;
-// font-size: 14px;
-//}
-//
-//body
-//{
-// font-family: 'Open Sans', sans-serif;
-// line-height: 1.5;
-// color: #4d4e53;
-// background-color: white;
-//}
-//
-//a, a:visited, a:active {
-// color: #0095dd;
-// text-decoration: none;
-//}
-//
-//a:hover {
-// text-decoration: underline;
-//}
-
-header
-{
- display: block;
- padding: 0px; //0px 4px;
-}
-
-tt, code, kbd, samp {
- font-family: Consolas, Monaco, 'Andale Mono', monospace;
-}
-
-.class-description {
- font-size: 130%;
- line-height: 140%;
- margin-bottom: 1em;
- margin-top: 1em;
-}
-
-.class-description:empty {
- margin: 0;
-}
-
-#main {
- float: left;
- width: 77.5%; //70%;
-}
-
-article dl {
- margin-bottom: 40px;
-}
-
-article img {
- max-width: 100%;
-}
-
-section
-{
- display: block;
- background-color: #fff;
- padding: 12px 0px; //12px 24px;
- border-bottom: 1px solid #ccc;
-// margin-right: 30px;
-}
-
-.variation {
- display: none;
-}
-
-.signature-attributes {
- font-size: 60%;
- color: #aaa;
- font-style: italic;
- font-weight: lighter;
-}
-
-nav
-{
- display: block;
- float: right;
- margin-top: 28px;
- width: 20%; //30%;
- box-sizing: border-box;
-// border-left: 1px solid #ccc;
- padding-left: 16px;
- background-color: #eee;
- padding-bottom: 16px;
-}
-
-//nav ul {
-// font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif;
-// font-size: 100%;
-// line-height: 17px;
-// padding: 0;
-// margin: 0;
-// list-style-type: none;
-//}
-//
-//nav ul a, nav ul a:visited, nav ul a:active {
-// font-family: Consolas, Monaco, 'Andale Mono', monospace;
-// line-height: 18px;
-// color: #4D4E53;
-//}
-
-nav h3 {
- margin-top: 12px;
-}
-
-nav li {
- margin-top: 6px;
-}
-
-footer {
- display: block;
- padding: 6px 0px; //6px;
- margin-top: 12px;
- font-style: italic;
- font-size: 90%;
-}
-
-//h1, h2, h3, h4 {
-// font-weight: 200;
-// margin: 0;
-//}
-//
-//h1
-//{
-// font-family: 'Open Sans Light', sans-serif;
-// font-size: 48px;
-// letter-spacing: -2px;
-// margin: 12px 24px 20px;
-//}
-//
-//h2, h3.subsection-title
-//{
-// font-size: 30px;
-// font-weight: 700;
-// letter-spacing: -1px;
-// margin-bottom: 12px;
-//}
-//
-//h3
-//{
-// font-size: 24px;
-// letter-spacing: -0.5px;
-// margin-bottom: 12px;
-//}
-//
-//h4
-//{
-// font-size: 18px;
-// letter-spacing: -0.33px;
-// margin-bottom: 12px;
-// color: #4d4e53;
-//}
-//
-//h5, .container-overview .subsection-title
-//{
-// font-size: 120%;
-// font-weight: bold;
-// letter-spacing: -0.01em;
-// margin: 8px 0 3px 0;
-//}
-//
-//h6
-//{
-// font-size: 100%;
-// letter-spacing: -0.01em;
-// margin: 6px 0 3px 0;
-// font-style: italic;
-//}
-
-table
-{
- border-spacing: 0;
- border: 0;
- border-collapse: collapse;
-}
-
-td, th
-{
- border: 1px solid #ddd;
- margin: 0px;
- text-align: left;
- vertical-align: top;
- padding: 4px 6px;
- display: table-cell;
-}
-
-thead tr
-{
- background-color: #ddd;
- font-weight: bold;
-}
-
-th { border-right: 1px solid #aaa; }
-tr > th:last-child { border-right: 1px solid #ddd; }
-
-.ancestors, .attribs { color: #999; }
-.ancestors a, .attribs a
-{
- color: #999 !important;
- text-decoration: none;
-}
-
-.clear
-{
- clear: both;
-}
-
-.important
-{
- font-weight: bold;
- color: #950B02;
-}
-
-.yes-def {
- text-indent: -1000px;
-}
-
-.type-signature {
- color: #aaa;
-}
-
-//.name, .signature {
-// font-family: Consolas, Monaco, 'Andale Mono', monospace;
-//}
-
-.details { margin-top: 14px; border-left: 2px solid #DDD; }
-.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; }
-.details dd { margin-left: 70px; }
-.details ul { margin: 0; }
-.details ul { list-style-type: none; }
-.details li { margin-left: 30px; padding-top: 6px; }
-.details pre.prettyprint { margin: 0 }
-.details .object-value { padding-top: 0; }
-
-.description {
- margin-bottom: 1em;
- margin-top: 1em;
-}
-
-.code-caption
-{
- font-style: italic;
- font-size: 107%;
- margin: 0;
-}
-
-.source
-{
- border: 1px solid #ddd;
- width: 77.5%; //80%;
- overflow: auto;
-}
-
-.prettyprint.source {
- width: inherit;
-}
-
-.source code
-{
- font-size: 100%;
- line-height: 18px;
- display: block;
- padding: 4px 12px;
- margin: 0;
- background-color: #fff;
- color: #4D4E53;
-}
-
-.prettyprint code span.line
-{
- display: inline-block;
-}
-
-.prettyprint.linenums
-{
- padding-left: 70px;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-
-.prettyprint.linenums ol
-{
- padding-left: 0;
-}
-
-.prettyprint.linenums li
-{
- border-left: 3px #ddd solid;
-}
-
-.prettyprint.linenums li.selected,
-.prettyprint.linenums li.selected *
-{
- background-color: lightyellow;
-}
-
-.prettyprint.linenums li *
-{
- -webkit-user-select: text;
- -moz-user-select: text;
- -ms-user-select: text;
- user-select: text;
-}
-
-.params .name, .props .name, .name code {
- color: #4D4E53;
- font-family: Consolas, Monaco, 'Andale Mono', monospace;
- font-size: 100%;
-}
-
-.params td.description > p:first-child,
-.props td.description > p:first-child
-{
- margin-top: 0;
- padding-top: 0;
-}
-
-.params td.description > p:last-child,
-.props td.description > p:last-child
-{
- margin-bottom: 0;
- padding-bottom: 0;
-}
-
-.disabled {
- color: #454545;
-}
-
-}
+++ /dev/null
-body {
- padding-top: 40px;
- padding-bottom: 40px;
- background-color: #eee;
-}
-
-.form-login {
- max-width: 330px;
- padding: 15px;
- margin: 0 auto;
-}
-.form-login .form-login-heading,
-.form-login .checkbox {
- margin-bottom: 10px;
-}
-.form-login .checkbox {
- font-weight: normal;
-}
-.form-login .form-control {
- position: relative;
- height: auto;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- padding: 10px;
- font-size: 16px;
-}
-.form-login .form-control:focus {
- z-index: 2;
-}
-.form-login input[type="email"] {
- margin-bottom: -1px;
- border-bottom-right-radius: 0;
- border-bottom-left-radius: 0;
-}
-.form-login input[type="password"] {
- margin-bottom: 10px;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
-}
if (signed_in_as !== undefined) {
// signed in
- p {'Your given names are visible to other users if you comment on our blog. Your email and family name remain private. If your name is one word or does not fit given names/family name pattern, then please enter given names only.'}
+ p/*.mt-3*/ {'Your given names are visible to other users if you comment on our blog. Your email and family name remain private. If your name is one word or does not fit given names/family name pattern, then please enter given names only.'}
div.accordion#accordion(role="tablist" aria-multiselectable="true") {
div.card#card-1 {
'Save'
}
- p.'mt-3'.mb-0#card-1-message(hidden) {}
+ div.alert.alert-danger.'mt-3'.mb-0#card-1-alert(hidden) {}
}
}
}
'Save'
}
- p.'mt-3'.mb-0#card-2-message(hidden) {}
+ div.alert.alert-danger.'mt-3'.mb-0#card-2-alert(hidden) {}
}
}
}
}
else {
// signed out
- p {'For account maintenance, please click on one of the options below.'}
+ p/*.mt-3*/ {'For account maintenance, please click on one of the options below.'}
let transaction = await env.site.database.Transaction()
try {
let children = await p.get('children')
let menu = await p.get_json('menu')
- ul.nav.flex-column {
+ nav {
for (let i = 0; i < menu.length; ++i) {
let name = menu[i]
let q = await children.get(name)
-
- li.nav-item {
- a.nav-link(href=`${name}/index.html`) {
- table.icon-and-text {
- tr {
- td {
- _out.push(
- await env.site.get_min_svg(await q.get_json('icon'))
- )
- }
- td {
- span.h2{
- `${await q.get_json('title')}`
- }
- }
+
+ // note: make the row a link to show hover background at edges
+ // (similar to the grid-gutter-background class I use elsewhere)
+ // note: position: relative sets the target for stretched link
+ div.row.flex-nowrap.align-items-center.nav-link-outer.position-relative {
+ div.col.col-icon128 {
+ div.img-fluid.py-3 {
+ _out.push(
+ await env.site.get_min_svg(await q.get_json('icon'))
+ )
+ }
+ }
+ div.col.col-text {
+ a.nav-link-inner.stretched-link(href=`${name}/index.html`) {
+ span.h2{
+ `${await q.get_json('title')}`
}
}
}
() => {
let id_accordion = document.getElementById('accordion')
let id_card_1 = document.getElementById('card-1')
+ let id_card_1_alert = document.getElementById('card-1-alert')
let id_card_1_collapse = document.getElementById('card-1-collapse')
let id_card_1_cross = document.getElementById('card-1-cross')
let id_card_1_form = document.getElementById('card-1-form')
let id_card_1_heading = document.getElementById('card-1-heading')
let id_card_1_icon = document.getElementById('card-1-icon')
- let id_card_1_message = document.getElementById('card-1-message')
let id_card_1_revert = document.getElementById('card-1-revert')
let id_card_1_save = document.getElementById('card-1-save')
let id_card_1_spinner = document.getElementById('card-1-spinner')
let id_card_1_tick = document.getElementById('card-1-tick')
let id_card_2 = document.getElementById('card-2')
+ let id_card_2_alert = document.getElementById('card-2-alert')
let id_card_2_clear = document.getElementById('card-2-clear')
let id_card_2_collapse = document.getElementById('card-2-collapse')
let id_card_2_cross = document.getElementById('card-2-cross')
let id_card_2_form = document.getElementById('card-2-form')
let id_card_2_heading = document.getElementById('card-2-heading')
let id_card_2_icon = document.getElementById('card-2-icon')
- let id_card_2_message = document.getElementById('card-2-message')
let id_card_2_save = document.getElementById('card-2-save')
let id_card_2_spinner = document.getElementById('card-2-spinner')
let id_card_2_tick = document.getElementById('card-2-tick')
id_card_1_tick.hidden = true
id_card_1_cross.hidden = true
id_card_1_spinner.hidden = true
- id_card_1_message.hidden = true
+ id_card_1_alert.hidden = true
}
id_given_names.addEventListener('input', card_1_edited)
id_card_1_tick.hidden = true
id_card_1_cross.hidden = true
id_card_1_spinner.hidden = true
- id_card_1_message.hidden = true
+ id_card_1_alert.hidden = true
}
)
id_card_1_tick.hidden = true
id_card_1_cross.hidden = true
id_card_1_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_card_1_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_card_1_alert.hidden = true
if (!id_card_1_form.checkValidity()) {
id_card_1_form.classList.add('was-validated');
id_card_1_cross.hidden = false
id_card_1_spinner.hidden = true
- id_card_1_message.textContent = problem.detail
- //id_card_1_message.classList.remove('text-success')
- id_card_1_message.classList.add('text-danger')
- id_card_1_message.hidden = false
+ id_card_1_alert.textContent = problem.detail
+ //id_card_1_alert.classList.remove('alert-success')
+ //id_card_1_alert.classList.add('alert-danger')
+ id_card_1_alert.hidden = false
return
}
id_card_1_tick.hidden = false
//id_card_1_tick.hidden = true
//id_card_1_cross.hidden = true
//id_card_1_spinner.hidden = true
- id_card_1_message.hidden = true
+ id_card_1_alert.hidden = true
}
)
id_card_2_tick.hidden = true
id_card_2_cross.hidden = true
id_card_2_spinner.hidden = true
- id_card_2_message.hidden = true
+ id_card_2_alert.hidden = true
}
id_old_password.addEventListener('input', card_2_edited)
id_card_2_tick.hidden = true
id_card_2_cross.hidden = true
id_card_2_spinner.hidden = true
- id_card_2_message.hidden = true
+ id_card_2_alert.hidden = true
}
)
id_card_2_tick.hidden = true
id_card_2_cross.hidden = true
id_card_2_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_card_2_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_card_2_alert.hidden = true
if (!id_card_2_form.checkValidity()) {
id_card_2_form.classList.add('was-validated');
id_card_2_cross.hidden = false
id_card_2_spinner.hidden = true
- id_card_2_message.textContent = problem.detail
- //id_card_2_message.classList.remove('text-success')
- id_card_2_message.classList.add('text-danger')
- id_card_2_message.hidden = false
+ id_card_2_alert.textContent = problem.detail
+ //id_card_2_alert.classList.remove('alert-success')
+ //id_card_2_alert.classList.add('alert-danger')
+ id_card_2_alert.hidden = false
return
}
id_card_2_tick.hidden = false
//id_card_2_tick.hidden = true
//id_card_2_cross.hidden = true
//id_card_2_spinner.hidden = true
- id_card_2_message.hidden = true
+ id_card_2_alert.hidden = true
}
)
}
async _out => {
await breadcrumbs(env, _out)
- p {'To reset your password, please enter new details below and we will send you a password reset link.'}
+ p/*.mt-3*/ {'To reset your password, please enter new details below and we will send you a password reset link.'}
form#form {
div.row {
}
if (email.length)
- button.btn.btn-success#password-reset(type="button") {
+ button.btn.btn-success.mb-3#password-reset(type="button") {
div.icon24-outer.mr-2#icon {
div.icon24-inner {_out.push(fa_envelope)}
}
'Password reset'
}
else
- button.btn.btn-success#password-reset(type="button" disabled) {
+ button.btn.btn-success.mb-3#password-reset(type="button" disabled) {
div.icon24-outer.mr-2#icon {
div.icon24-inner {_out.push(fa_envelope)}
}
'Password reset'
}
- p.'mt-3'.mb-0#message(hidden) {}
+ div.alert#alert(hidden) {}
- p.text-muted.mt-3 {'* These fields are required.'}
+ p.text-muted {'* These fields are required.'}
},
// scripts
async _out => {
document.addEventListener(
'DOMContentLoaded',
() => {
+ let id_alert = document.getElementById('alert')
let id_cross = document.getElementById('cross')
let id_email = document.getElementById('email')
let id_form = document.getElementById('form')
let id_icon = document.getElementById('icon')
- let id_message = document.getElementById('message')
let id_new_password = document.getElementById('new-password')
let id_password_reset = document.getElementById('password-reset')
let id_spinner = document.getElementById('spinner')
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- id_message.hidden = true
+ id_alert.hidden = true
}
id_email.addEventListener('input', edited)
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_alert.hidden = true
if (!id_form.checkValidity()) {
id_form.classList.add('was-validated');
id_cross.hidden = false
id_spinner.hidden = true
- id_message.textContent = problem.detail
- //id_message.classList.remove('text-success')
- id_message.classList.add('text-danger')
- id_message.hidden = false
+ id_alert.textContent = problem.detail
+ id_alert.classList.remove('alert-success')
+ id_alert.classList.add('alert-danger')
+ id_alert.hidden = false
return
}
id_tick.hidden = false
id_spinner.hidden = true
- id_message.textContent = `Password reset link has been sent to "${email}". Please check your email for next steps.`
- //id_message.classList.add('text-success')
- id_message.classList.remove('text-danger')
- id_message.hidden = false
+ id_alert.textContent = `Password reset link has been sent to "${email}". Please check your email for next steps.`
+ id_alert.classList.add('alert-success')
+ id_alert.classList.remove('alert-danger')
+ id_alert.hidden = false
}
)
}
async _out => {
await breadcrumbs(env, _out)
- p {'Your email is not yet verified. Check your email for next steps, or re-send the verification email below.'}
+ p/*.mt-3*/ {'Your email is not yet verified. Check your email for next steps, or re-send the verification email below.'}
form#form {
div.row.align-items-center {
}
}
- p#message(hidden) {}
+ div.alert#alert(hidden) {}
},
// scripts
async _out => {
document.addEventListener(
'DOMContentLoaded',
() => {
+ let id_alert = document.getElementById('alert')
let id_cross = document.getElementById('cross')
let id_email = document.getElementById('email')
let id_form = document.getElementById('form')
let id_icon = document.getElementById('icon')
- let id_message = document.getElementById('message')
let id_send_verification_email = document.getElementById('send-verification-email')
let id_spinner = document.getElementById('spinner')
let id_tick = document.getElementById('tick')
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- id_message.hidden = true
+ id_alert.hidden = true
}
id_email.addEventListener('input', edited)
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_alert.hidden = true
if (!id_form.checkValidity()) {
id_form.classList.add('was-validated');
id_cross.hidden = false
id_spinner.hidden = true
- id_message.textContent = problem.detail
- //id_message.classList.remove('text-success')
- id_message.classList.add('text-danger')
- id_message.hidden = false
+ id_alert.textContent = problem.detail
+ id_alert.classList.remove('alert-success')
+ id_alert.classList.add('alert-danger')
+ id_alert.hidden = false
return
}
id_tick.hidden = false
id_spinner.hidden = true
- id_message.textContent = `Email verification link has been sent to "${email}". Please check your email for next steps.`
- //id_message.classList.add('text-success')
- id_message.classList.remove('text-danger')
- id_message.hidden = false
+ id_alert.textContent = `Email verification link has been sent to "${email}". Please check your email for next steps.`
+ id_alert.classList.add('alert-success')
+ id_alert.classList.remove('alert-danger')
+ id_alert.hidden = false
}
)
}
async _out => {
await breadcrumbs(env, _out)
- p {'Signing up allows you to leave comments on our blog and receive communications from us.'}
+ p/*.mt-3*/ {'Signing up allows you to leave comments on our blog and receive communications from us.'}
p {'Your given names are visible to other users if you comment on our blog. Your email and family name remain private. If your name is one word or does not fit given names/family name pattern, then please enter given names only.'}
'Create account'
}
- p.'mt-3'.mb-0#card-1-message(hidden) {}
+ div.alert.'mt-3'.mb-0#card-1-alert(hidden) {}
}
}
}
'Re-send email'
}
- p.'mt-3'.mb-0#card-2-message(hidden) {}
+ div.alert.'mt-3'.mb-0#card-2-alert(hidden) {}
}
}
}
() => {
let id_accordion = document.getElementById('accordion')
let id_card_1 = document.getElementById('card-1')
+ let id_card_1_alert = document.getElementById('card-1-alert')
let id_card_1_collapse = document.getElementById('card-1-collapse')
let id_card_1_create_account = document.getElementById('card-1-create-account')
let id_card_1_cross = document.getElementById('card-1-cross')
let id_card_1_heading = document.getElementById('card-1-heading')
- let id_card_1_message = document.getElementById('card-1-message')
let id_card_1_new = document.getElementById('card-1-new')
let id_card_1_spinner = document.getElementById('card-1-spinner')
let id_card_1_tick = document.getElementById('card-1-tick')
let id_card_2 = document.getElementById('card-2')
+ let id_card_2_alert = document.getElementById('card-2-alert')
let id_card_2_back = document.getElementById('card-2-back')
let id_card_2_collapse = document.getElementById('card-2-collapse')
let id_card_2_cross = document.getElementById('card-2-cross')
let id_card_2_heading = document.getElementById('card-2-heading')
- let id_card_2_message = document.getElementById('card-2-message')
let id_card_2_resend_email = document.getElementById('card-2-resend-email')
let id_card_2_spinner = document.getElementById('card-2-spinner')
let id_card_2_tick = document.getElementById('card-2-tick')
id_card_1_tick.hidden = true
id_card_1_cross.hidden = true
id_card_1_spinner.hidden = true
- id_card_1_message.hidden = true
+ id_card_1_alert.hidden = true
id_card_2_resend_email.disabled = true
id_card_2_tick.hidden = true
id_card_2_cross.hidden = true
id_card_2_spinner.hidden = true
- id_card_2_message.hidden = true
+ id_card_2_alert.hidden = true
}
let details
id_card_1_tick.hidden = true
id_card_1_cross.hidden = true
id_card_1_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_card_1_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_card_1_alert.hidden = true
if (!id_form.checkValidity()) {
// workaround for https://github.com/twbs/bootstrap/issues/23454
id_card_1_cross.hidden = false
id_card_1_spinner.hidden = true
- id_card_1_message.textContent = problem.detail
- //id_card_1_message.classList.remove('text-success')
- id_card_1_message.classList.add('text-danger')
- id_card_1_message.hidden = false
+ id_card_1_alert.textContent = problem.detail
+ id_card_1_alert.classList.remove('alert-success')
+ id_card_1_alert.classList.add('alert-danger')
+ id_card_1_alert.hidden = false
$('#card-1-collapse').collapse('show')
return false
}
id_card_1_tick.hidden = false
id_card_1_spinner.hidden = true
- id_card_1_message.textContent = `Your account with email "${details.email}" has been created.`
- //id_card_1_message.classList.add('text-success')
- id_card_1_message.classList.remove('text-danger')
- id_card_1_message.hidden = false
+ id_card_1_alert.textContent = `Your account with email "${details.email}" has been created.`
+ id_card_1_alert.classList.add('alert-success')
+ id_card_1_alert.classList.remove('alert-danger')
+ id_card_1_alert.hidden = false
id_card_2_resend_email.disabled = false
id_card_2.scrollIntoView()
id_card_2_tick.hidden = true
id_card_2_cross.hidden = true
id_card_2_spinner.hidden = false
- // the below causes an ugly flicker, so just keep the message
- //id_card_2_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_card_2_alert.hidden = true
try {
await api_call(
id_card_2_cross.hidden = false
id_card_2_spinner.hidden = true
- id_card_2_message.textContent = problem.detail
- //id_card_2_message.classList.remove('text-success')
- id_card_2_message.classList.add('text-danger')
- id_card_2_message.hidden = false
+ id_card_2_alert.textContent = problem.detail
+ id_card_2_alert.classList.remove('alert-success')
+ id_card_2_alert.classList.add('alert-danger')
+ id_card_2_alert.hidden = false
$('#card-2-collapse').collapse('show')
return false
id_card_2_tick.hidden = false
id_card_2_spinner.hidden = true
- id_card_2_message.textContent = `Email verification link has been sent to "${details.email}". Please check your email for next steps.`
- //id_card_2_message.classList.add('text-success')
- id_card_2_message.classList.remove('text-danger')
- id_card_2_message.hidden = false
+ id_card_2_alert.textContent = `Email verification link has been sent to "${details.email}". Please check your email for next steps.`
+ id_card_2_alert.classList.add('alert-success')
+ id_card_2_alert.classList.remove('alert-danger')
+ id_card_2_alert.hidden = false
return true
}
async _out => {
await breadcrumbs(env, _out)
- p {'You will need to verify your email address via an emailed link before you can sign in to your account.'}
+ p/*.mt-3*/ {'You will need to verify your email address via an emailed link before you can sign in to your account.'}
form#form {
div.row {
}
if (email.length || details.length)
- button.btn.btn-success#verify-email(type="button") {
+ button.btn.btn-success.mb-3#verify-email(type="button") {
div.icon24-outer.mr-2#icon {
div.icon24-inner {_out.push(fa_user_check)}
}
'Verify email'
}
else
- button.btn.btn-success#verify-email(type="button" disabled) {
+ button.btn.btn-success.mb-3#verify-email(type="button" disabled) {
div.icon24-outer.mr-2#icon {
div.icon24-inner {_out.push(fa_user_check)}
}
'Verify email'
}
- p.'mt-3'.mb-0#message(hidden) {}
+ div.alert#alert(hidden) {}
- p.text-muted.mt-3 {'* These fields are required.'}
+ p.text-muted {'* These fields are required.'}
},
// scripts
async _out => {
document.addEventListener(
'DOMContentLoaded',
() => {
+ let id_alert = document.getElementById('alert')
let id_cross = document.getElementById('cross')
let id_email = document.getElementById('email')
let id_form = document.getElementById('form')
let id_icon = document.getElementById('icon')
let id_link_code = document.getElementById('link-code')
- let id_message = document.getElementById('message')
let id_spinner = document.getElementById('spinner')
let id_tick = document.getElementById('tick')
let id_verify_email = document.getElementById('verify-email')
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- id_message.hidden = true
+ id_alert.hidden = true
}
id_email.addEventListener('input', edited)
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_alert.hidden = true
if (!id_form.checkValidity()) {
id_form.classList.add('was-validated');
id_cross.hidden = false
id_spinner.hidden = true
- id_message.textContent = problem.detail
- //id_message.classList.remove('text-success')
- id_message.classList.add('text-danger')
- id_message.hidden = false
+ id_alert.textContent = problem.detail
+ id_alert.classList.remove('alert-success')
+ id_alert.classList.add('alert-danger')
+ id_alert.hidden = false
return
}
id_tick.hidden = false
id_spinner.hidden = true
- id_message.textContent = `Your email "${email}" has been verified. You can now sign in.`
- //id_message.classList.add('text-success')
- id_message.classList.remove('text-danger')
- id_message.hidden = false
+ id_alert.textContent = `Your email "${email}" has been verified. You can now sign in.`
+ id_alert.classList.add('alert-success')
+ id_alert.classList.remove('alert-danger')
+ id_alert.hidden = false
}
)
}
async _out => {
await breadcrumbs(env, _out)
- p {'You will need to verify your new password via an emailed link before you can use it to sign in to your account.'}
+ p/*.mt-3*/ {'You will need to verify your new password via an emailed link before you can use it to sign in to your account.'}
form#form {
div.row {
}
if (email.length || details.length)
- button.btn.btn-success#verify-password(type="button") {
+ button.btn.btn-success.mb-3#verify-password(type="button") {
div.icon24-outer.mr-2#icon {
div.icon24-inner {_out.push(fa_user_check)}
}
'Verify password'
}
else
- button.btn.btn-success#verify-password(type="button" disabled) {
+ button.btn.btn-success.mb-3#verify-password(type="button" disabled) {
div.icon24-outer.mr-2#icon {
div.icon24-inner {_out.push(fa_user_check)}
}
'Verify password'
}
- p.'mt-3'.mb-0#message(hidden) {}
+ div.alert#alert(hidden) {}
- p.text-muted.mt-3 {'* These fields are required.'}
+ p.text-muted {'* These fields are required.'}
},
// scripts
async _out => {
document.addEventListener(
'DOMContentLoaded',
() => {
+ let id_alert = document.getElementById('alert')
let id_cross = document.getElementById('cross')
let id_email = document.getElementById('email')
let id_form = document.getElementById('form')
let id_icon = document.getElementById('icon')
let id_link_code = document.getElementById('link-code')
- let id_message = document.getElementById('message')
let id_spinner = document.getElementById('spinner')
let id_tick = document.getElementById('tick')
let id_verify_password = document.getElementById('verify-password')
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- id_message.hidden = true
+ id_alert.hidden = true
}
id_email.addEventListener('input', edited)
id_tick.hidden = true
id_cross.hidden = true
id_spinner.hidden = true
- // the below causes an ugly flicker, so just keep the message
- //id_message.hidden = true
+ // the below causes an ugly flicker, so just keep the alert
+ //id_alert.hidden = true
if (!id_form.checkValidity()) {
id_form.classList.add('was-validated');
id_cross.hidden = false
id_spinner.hidden = true
- id_message.textContent = problem.detail
- //id_message.classList.remove('text-success')
- id_message.classList.add('text-danger')
- id_message.hidden = false
+ id_alert.textContent = problem.detail
+ id_alert.classList.remove('alert-success')
+ id_alert.classList.add('alert-danger')
+ id_alert.hidden = false
return
}
id_tick.hidden = false
id_spinner.hidden = true
- id_message.textContent = `New password for "${email}" has been verified. You can now sign in.`
- //id_message.classList.add('text-success')
- id_message.classList.remove('text-danger')
- id_message.hidden = false
+ id_alert.textContent = `New password for "${email}" has been verified. You can now sign in.`
+ id_alert.classList.add('alert-success')
+ id_alert.classList.remove('alert-danger')
+ id_alert.hidden = false
}
)
}
return async env => {
let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
- let icon_jst = await env.site.get_min_svg('/_svg/icon_jst.svg')
+ let get_navigation = await _require('/_lib/get_navigation.jst')
let navbar = await _require('/_lib/navbar.jst')
- let icon_pitree = await env.site.get_min_svg('/_svg/icon_pitree.svg')
await navbar(
env,
i {'[Actually don\'t because it\'s still under construction]'}
}
- ul.nav.flex-column {
- li.nav-item {
- a.nav-link {
- table.icon-and-text {
- tr {
- td {
- _out.push(icon_pitree)
- }
- td {
- span.h1 {
- span.serif {'Ï€'}
- 'tree'
- }
- br {}
- span.h3 {'the source code analysis and transformation framework'}
+ let transaction = await env.site.database.Transaction()
+ try {
+ let root = await transaction.get()
+ let p = await get_navigation(root, env.component_names)
+ let children = await p.get('children')
+ let menu = await p.get_json('menu')
+
+ nav {
+ for (let i = 0; i < menu.length; ++i) {
+ let name = menu[i]
+ let q = await children.get(name)
+
+ // note: make the row a link to show hover background at edges
+ // (similar to the grid-gutter-background class I use elsewhere)
+ // note: position: relative sets the target for stretched link
+ div.row.flex-nowrap.align-items-center.nav-link-outer.position-relative {
+ div.col.col-icon128 {
+ div.img-fluid.py-3 {
+ _out.push(
+ await env.site.get_min_svg(await q.get_json('icon'))
+ )
}
}
- }
- }
- }
- li.nav-item {
- a.nav-link {
- table.icon-and-text {
- tr {
- td {
- _out.push(icon_jst)
- }
- td {
- span.h1{'JST'}
+ div.col.col-text {
+ a.nav-link-inner.stretched-link(href=`${name}/index.html`) {
+ span.h2{
+ `${await q.get_json('title')}`
+ }
br {}
- span.h3{'the web development framework with JavaScript Templates'}
+ span.h4{
+ `${await q.get_json('description')}`
+ }
+
}
}
}
}
}
}
+ finally {
+ transaction.rollback()
+ }
},
// scripts
async _out => {}
--- /dev/null
+return async env => {
+ let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
+ let navbar = await _require('/_lib/navbar.jst')
+
+ await navbar(
+ env,
+ // head
+ async _out => {},
+ // body
+ async _out => {
+ await breadcrumbs(env, _out)
+
+ p {
+ i {'Under construction.'}
+ }
+ },
+ // scripts
+ async _out => {}
+ )
+}
--- /dev/null
+return async env => {
+ let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
+ let navbar = await _require('/_lib/navbar.jst')
+
+ await navbar(
+ env,
+ // head
+ async _out => {},
+ // body
+ async _out => {
+ await breadcrumbs(env, _out)
+
+ p {
+ i {'Under construction.'}
+ }
+ },
+ // scripts
+ async _out => {}
+ )
+}
await breadcrumbs(env, _out)
- h4 {
+ h4/*.mt-3*/ {
'Query: '
- strong {`${query}`}
+ b {`${query}`}
}
if (search.results.length) {
ol(start=first + 1) {
for (let i = 0; i < search.results.length; ++i) {
let page = search.results[i].auxiliary
- li {
+ li.mb-3 {
a(href=page) {`${await breadcrumbs_str(page)}`}
br {}
- p {_out.push(search.results[i].summary)} // note: contains HTML
+ _out.push(search.results[i].summary) // note: contains HTML
}
}
}