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 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 logo_large = await env.site.get_min_svg('/_svg/logo_large.svg')
let page = await _require('/_lib/page.jst')
// initialize env.cart
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#navbar-sidebar-outer {
- nav.sidebar-inner.d-flex.flex-column#navbar-sidebar-inner {
- div.mb-4 {
- div(style="width: 128px; height: 128px;") {
- _out.push(avatar_maker)
- }
- b.h1 {
- `${site_title}`
- }
+ div.scrollbar-fix-outer {
+ div.scrollbar-fix-inner {
+ div.container {
+ div.row.align-items-center.py-3 {
+ div.col-sm-8 {
+ _out.push(logo_large)
}
-
- div.mb-2 {
- span#navbar-signed-in-status {
+ div.'col-sm-4' {
+ div.'mb-1'.text-right {
+ span#navbar-signed-in-status {
+ if (signed_in_as !== undefined)
+ 'Signed in.' //`Signed in as ${signed_in_as}.`
+ else
+ 'Browsing as guest.'
+ }
+ ' '
+ if (signed_in_as !== undefined)
+ a#navbar-sign-in(href="#" hidden) {'Sign in'}
+ else
+ a#navbar-sign-in(href="#") {'Sign in'}
+ ' '
+ if (signed_in_as !== undefined)
+ a#navbar-sign-up(href="/my_account/sign_up/index.html" hidden) {'Sign up'}
+ else
+ a#navbar-sign-up(href="/my_account/sign_up/index.html") {'Sign up'}
+ ' '
if (signed_in_as !== undefined)
- 'Signed in.'
+ a#navbar-sign-out(href="#") {'Sign out'}
else
- 'Signed out.'
+ a#navbar-sign-out(href="#" hidden) {'Sign out'}
}
- ' '
- if (signed_in_as !== undefined)
- a#navbar-sign-in(href="#" hidden) {'Sign in'}
- else
- a#navbar-sign-in(href="#") {'Sign in'}
- ' '
- if (signed_in_as !== undefined)
- a#navbar-sign-up(href="/my_account/sign_up/index.html" hidden) {'Sign up'}
- else
- a#navbar-sign-up(href="/my_account/sign_up/index.html") {'Sign up'}
- ' '
- if (signed_in_as !== undefined)
- a#navbar-sign-out(href="#") {'Sign out'}
- else
- a#navbar-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#navbar-search-button(type="submit") {
- div.icon24-outer {
- div.icon24-inner {_out.push(fa_search)}
+ form(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#navbar-search-button(type="submit") {
+ div.icon24-outer {
+ div.icon24-inner {_out.push(fa_search)}
+ }
}
}
}
}
}
- // the active entry in the navbar 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)'}
- }
+ //div.'col-sm-1'.vbottom {
+ // // a nested div is used to avoid hover colour on the padding
+ // div.nav-li-a(style="text-align: center;") {
+ // a(href="/online_store/view_cart/index.html") {
+ // div.cart-icon {
+ // _out.push(icon_cart_small)
+ // }
+ // div.cart-number {
+ // div.cart-circle {
+ // `${(env.cart.items || []).length}`
+ // }
+ // }
+ // }
+ // }
+ //}
+ }
+ 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") {
+ span.navbar-toggler-icon {}
+ }
+ div.collapse.navbar-collapse#navbarSupportedContent {
+ ul.navbar-nav.mr-auto {
+ // the active entry in the navbar 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))
+ li.nav-item.active {
+ a.nav-link(href=menu_prefix_path) {
+ `${menu_titles[i]}`
+ span.sr-only {' (current)'}
+ }
+ }
+ else
+ li.nav-item {
+ a.nav-link(href=menu_prefix_path) {
+ `${menu_titles[i]}`
+ }
+ }
}
- else
- div.nav-item {
- a.nav-link.nav-link2.grid-gutter-background(href=menu_prefix_path) {
- `${menu_titles[i]}`
- }
+ //li.nav-item.dropdown {
+ // a.nav-link.dropdown-toggle#navbarDropdown(href="#" role="button" data-toggle="dropdown" aria-expanded="false") {
+ // 'Dropdown'
+ // }
+ // div.dropdown-menu(aria-labelledby="navbarDropdown") {
+ // a.dropdown-item(href="#") {
+ // 'Action'
+ // }
+ // ' '
+ // a.dropdown-item(href="#") {
+ // 'Another action'
+ // }
+ // div.dropdown-divider {}
+ // a.dropdown-item(href="#") {
+ // 'Something else here'
+ // }
+ // }
+ //}
+ //li.nav-item {
+ // a.nav-link.disabled {
+ // 'Disabled'
+ // }
+ //}
+ }
+ ul.navbar-nav.ml-auto {
+ li.nav-item {
+ a.nav-link#navbar-give-feedback(href="#") {'Give feedback'}
}
- }
- div.nav-item.mt-auto {
- a.nav-link.nav-link2.grid-gutter-background#navbar-give-feedback(href="#") {'Give feedback'}
- }
- }
- }
-
- div.col-md.sidebar-content {
- // the breadcrumbs have already been determined by navbar.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#navbar-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 {
+ 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") {}
}
p {
'This website is '
- a(href="https://git.ndcode.org/public/nick_site.git") {
+ a(href="https://git.ndcode.org/public/ndcode_site.git") {
'open source'
}
' and licensed under a '
}
'.'
}
-
+
+ p {'Example code fragments embedded within the text are placed in the public domain unless otherwise noted.'}
+
p.mb-0 {`Copyright © ${new XDate(env.now).getUTCFullYear()} ${copyright}.`}
}
}
- div.col-md.sidebar-dummy {}
}
}
let id_navbar_message_modal = document.getElementById('navbar-message-modal')
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_sidebar_inner = document.getElementById('navbar-sidebar-inner')
- let id_navbar_sidebar_outer = document.getElementById('navbar-sidebar-outer')
- let id_navbar_sidebar_toggle = document.getElementById('navbar-sidebar-toggle')
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_out = document.getElementById('navbar-sign-out')
let id_navbar_sign_up = document.getElementById('navbar-sign-up')
let id_navbar_signed_in_status = document.getElementById('navbar-signed-in-status')
+ //let id_navbarDropdown = document.getElementById('navbarDropdown')
+ //let id_navbarSupportedContent = document.getElementById('navbarSupportedContent')
// sign in form
id_navbar_sign_in.addEventListener(
$('#navbar-message-modal').modal('show')
}
)
-
- // sidebar
- let sidebar_outer_computed_style = window.getComputedStyle(
- id_navbar_sidebar_outer
- )
- let sidebar_toggle_computed_style = window.getComputedStyle(
- id_navbar_sidebar_toggle
- )
- let sidebar_is_collapsed =
- () =>
- sidebar_toggle_computed_style.display !== 'none' &&
- id_navbar_sidebar_outer.classList.contains(
- 'sidebar-outer-collapsed'
- )
- let sidebar_collapse_update = () => {
- if (sidebar_outer_computed_style.position === 'sticky') { // md and up
- id_navbar_sidebar_outer.style.flexBasis =
- sidebar_is_collapsed() ?
- '0px' :
- `${id_navbar_sidebar_inner.clientWidth}px`
- id_navbar_sidebar_outer.style.removeProperty('height')
- }
- else {
- id_navbar_sidebar_outer.style.height =
- sidebar_is_collapsed() ?
- '0px' :
- `${id_navbar_sidebar_inner.clientHeight}px`
- id_navbar_sidebar_outer.style.removeProperty('flex-basis')
- }
- }
- window.addEventListener('resize', sidebar_collapse_update)
- sidebar_collapse_update()
-
- id_navbar_sidebar_outer.addEventListener(
- 'transitionend',
- () => {
- // transitions are only allowed after clicking collapse button,
- // otherwise they can be triggered by media queries on resize
- id_navbar_sidebar_outer.style.removeProperty('transition-property')
- }
- )
-
- id_navbar_sidebar_toggle.addEventListener(
- 'click',
- () => {
- if (
- id_navbar_sidebar_outer.classList.contains(
- 'sidebar-outer-collapsed'
- )
- )
- id_navbar_sidebar_outer.classList.remove(
- 'sidebar-outer-collapsed'
- )
- else
- id_navbar_sidebar_outer.classList.add(
- 'sidebar-outer-collapsed'
- )
- id_navbar_sidebar_outer.style.transitionProperty =
- sidebar_outer_computed_style.position === 'sticky' ?
- 'flex-basis' : // md and up
- 'height'
- sidebar_collapse_update()
- }
- )
}
)
}
--- /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)
+ }
+ )
+}