1 let assert = require('assert')
2 let XDate = require('xdate')
4 return async (env, head, body, scripts) => {
5 //let cart = await _require('/online_store/cart.jst')
6 let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
7 let fa_times_circle = await env.site.get_min_svg('/_svg/fa_times-circle.svg')
8 let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
9 let fa_unlock_alt = await env.site.get_min_svg('/_svg/fa_unlock-alt.svg')
10 let fa_search = await env.site.get_min_svg('/_svg/fa_search.svg')
11 let get_session = await _require('/_lib/get_session.jst')
12 //let icon_cart_small = await env.site.get_min_svg('/_svg/icon_cart_small.svg')
13 let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
14 //let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
15 let logo_large = await env.site.get_min_svg('/_svg/logo_large.svg')
16 let menu = await env.site.get_menu('/_menu.json')
17 let page = await _require('/_lib/page.jst')
19 // initialize env.cart
22 let transaction = await env.site.database.Transaction()
24 let site_title, copyright
27 let root = await transaction.get({})
29 let session = await get_session(env, root)
30 signed_in_as = await session.get_json('signed_in_as')
32 let globals = await root.get('globals', {})
33 site_title = await globals.get_json('site_title')
34 copyright = await globals.get_json('copyright')
36 feedback_draft = await session.get_json('feedback_draft')
37 if (feedback_draft === undefined || env.now >= feedback_draft.expires)
41 transaction.rollback()
48 // extract top-level directory name
49 assert(env.parsed_url.pathname.slice(0, 1) === '/')
50 let index = env.parsed_url.pathname.indexOf('/', 1)
51 let dir = index === -1 ? '' : env.parsed_url.pathname.slice(1, index)
57 menu.entries[menu.index[dir]].name
65 // extract top-level directory name
66 assert(env.parsed_url.pathname.slice(0, 1) === '/')
67 let index = env.parsed_url.pathname.indexOf('/', 1)
68 let dir = index === -1 ? '' : env.parsed_url.pathname.slice(1, index)
72 div.row.align-items-center.py-3 {
77 div.'mb-1'.text-right {
78 span#navbar-signed-in-status {
79 if (signed_in_as !== undefined)
80 'Signed in.' //`Signed in as ${signed_in_as}.`
85 if (signed_in_as !== undefined)
86 a#navbar-sign-in(href="#" hidden) {'Sign in'}
88 a#navbar-sign-in(href="#") {'Sign in'}
90 if (signed_in_as !== undefined)
91 a#navbar-sign-up(href="/my_account/sign_up/index.html" hidden) {'Sign up'}
93 a#navbar-sign-up(href="/my_account/sign_up/index.html") {'Sign up'}
95 if (signed_in_as !== undefined)
96 a#navbar-sign-out(href="#") {'Sign out'}
98 a#navbar-sign-out(href="#" hidden) {'Sign out'}
101 form(action="/search/index.html") {
103 input.form-control(name="query" type="text" placeholder="Search" aria-describedby="search-button") {}
104 div.input-group-append {
105 button.btn.btn-outline-secondary#navbar-search-button(type="submit") {
107 div.icon24-inner {_out.push(fa_search)}
115 //div.'col-sm-1'.vbottom {
116 // // a nested div is used to avoid hover colour on the padding
117 // div.nav-li-a(style="text-align: center;") {
118 // a(href="/online_store/view_cart/index.html") {
120 // _out.push(icon_cart_small)
124 // `${(env.cart.items || []).length}`
133 nav.navbar.navbar-expand-lg.navbar-dark.bg-primary.scrollbar-fix {
135 //a.navbar-brand(href="#") {'Navbar'}
137 button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation") {
138 span.navbar-toggler-icon {}
140 div.collapse.navbar-collapse#navbarSupportedContent {
141 ul.navbar-nav.mr-auto {
142 if (dir.length === 0)
144 a.nav-link(href="/index.html") {
146 span.sr-only {' (current)'}
151 a.nav-link(href="/index.html") {'Home'}
153 let entries = menu.entries
154 for (let i = 0; i < entries.length; ++i)
155 if (entries[i].navbar)
156 if (entries[i].dir === dir)
158 a.nav-link(href=`/${entries[i].dir}/index.html`) {
160 span.sr-only {' (current)'}
165 a.nav-link(href=`/${entries[i].dir}/index.html`) {
169 //li.nav-item.dropdown {
170 // a.nav-link.dropdown-toggle#navbarDropdown(href="#" role="button" data-toggle="dropdown" aria-expanded="false") {
173 // div.dropdown-menu(aria-labelledby="navbarDropdown") {
174 // a.dropdown-item(href="#") {
178 // a.dropdown-item(href="#") {
181 // div.dropdown-divider {}
182 // a.dropdown-item(href="#") {
183 // 'Something else here'
188 // a.nav-link.disabled {
193 ul.navbar-nav.ml-auto {
195 a.nav-link#navbar-give-feedback(href="#") {'Give feedback'}
206 footer.scrollbar-fix {
208 a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
209 img(alt="Creative Commons License" style="border-width:0;" src="/images/by-sa_3.0_88x31.png") {}
213 a(href="https://git.ndcode.org/public/ndcode_site.git") {
216 ' and licensed under a '
217 a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
218 'Creative Commons Attribution-ShareAlike 3.0 Unported License'
223 p {'Example code fragments embedded within the text are placed in the public domain unless otherwise noted.'}
225 p {`Copyright © ${new XDate(env.now).getUTCFullYear()} ${copyright}.`}
230 div.modal#navbar-sign-in-modal(tabindex="-1") {
234 span.h4.modal-title {'Sign in'}
237 form#navbar-sign-in-form {
241 label.form-label(for="navbar-sign-in-email") {'Email'}
242 input.form-control#navbar-sign-in-email(type="email" required maxlength=256) {}
243 div.invalid-feedback {'Please enter your account\'s email address.'}
250 label.form-label(for="navbar-sign-in-password") {'Password'}
251 input.form-control#navbar-sign-in-password(type="password" required minlength=8 maxlength=256) {}
252 div.invalid-feedback {'Please enter at least 8 characters.'}
260 a(href="/my_account/sign_up/index.html") {'Sign up'}
265 a(href="/my_account/password_reset/index.html") {'Password reset'}
268 p.'mt-3'.mb-0#navbar-sign-in-message(hidden) {}
271 button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
272 div.icon24-outer.mr-2 {
273 div.icon24-inner {_out.push(fa_arrow_circle_left)}
277 button.btn.btn-primary#navbar-sign-in-sign-in(type="button") {
278 div.icon24-outer.mr-2#navbar-sign-in-icon {
279 div.icon24-inner {_out.push(fa_unlock_alt)}
281 //div.icon24-outer.mr-2#navbar-sign-in-tick(hidden) {
282 // div.icon24-inner {_out.push(icon_tick)}
284 div.icon24-outer.mr-2#navbar-sign-in-cross(hidden) {
285 div.icon24-inner {_out.push(icon_cross)}
287 div.icon24-outer.mr-2#navbar-sign-in-spinner(hidden) {
289 div.spinner-border.spinner-border-sm(role="status") {}
299 div.modal#navbar-feedback-modal(tabindex="-1") {
303 span.h4.modal-title {'Give feedback'}
307 'Did you notice something not quite right, or just want to share your impression of this page?'
310 form#navbar-feedback-form {
314 label.form-label(for="navbar-feedback-message1") {'Message'}
315 textarea.form-control#navbar-feedback-message1(placeholder="I noticed that..." required rows=4 maxlength=65536) {
317 `${feedback_draft.message}`
319 div.invalid-feedback {'Please let us have your thoughts.'}
325 p.'mt-3'.mb-0#navbar-feedback-message(hidden) {}
328 button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
329 div.icon24-outer.mr-2 {
330 div.icon24-inner {_out.push(fa_arrow_circle_left)}
335 button.btn.btn-primary#navbar-feedback-send-message(type="button") {
336 div.icon24-outer.mr-2#navbar-feedback-icon {
337 div.icon24-inner {_out.push(fa_envelope)}
339 //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
340 // div.icon24-inner {_out.push(icon_tick)}
342 div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
343 div.icon24-inner {_out.push(icon_cross)}
345 div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
347 div.spinner-border.spinner-border-sm(role="status") {}
353 button.btn.btn-primary#navbar-feedback-send-message(type="button" disabled) {
354 div.icon24-outer.mr-2#navbar-feedback-icon {
355 div.icon24-inner {_out.push(fa_envelope)}
357 //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
358 // div.icon24-inner {_out.push(icon_tick)}
360 div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
361 div.icon24-inner {_out.push(icon_cross)}
363 div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
365 div.spinner-border.spinner-border-sm(role="status") {}
375 div.modal#navbar-message-modal(tabindex="-1") {
379 span.h4.modal-title {'Message'}
381 div.modal-body#navbar-message-modal-message {
384 button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
385 div.icon24-outer.mr-2 {
386 div.icon24-inner {_out.push(fa_times_circle)}
397 script(src="/js/utils.js") {}
400 // this function can be overridden in a further script
401 function sign_in_out(status) {
404 document.addEventListener(
407 let id_navbar_feedback_cross = document.getElementById('navbar-feedback-cross')
408 let id_navbar_feedback_form = document.getElementById('navbar-feedback-form')
409 let id_navbar_feedback_icon = document.getElementById('navbar-feedback-icon')
410 let id_navbar_feedback_message = document.getElementById('navbar-feedback-message')
411 let id_navbar_feedback_message1 = document.getElementById('navbar-feedback-message1')
412 let id_navbar_feedback_modal = document.getElementById('navbar-feedback-modal')
413 let id_navbar_feedback_send_message = document.getElementById('navbar-feedback-send-message')
414 let id_navbar_feedback_spinner = document.getElementById('navbar-feedback-spinner')
415 let id_navbar_feedback_tick = document.getElementById('navbar-feedback-tick')
416 let id_navbar_give_feedback = document.getElementById('navbar-give-feedback')
417 let id_navbar_message_modal = document.getElementById('navbar-message-modal')
418 let id_navbar_message_modal_message = document.getElementById('navbar-message-modal-message')
419 let id_navbar_search_button = document.getElementById('navbar-search-button')
420 let id_navbar_sign_in = document.getElementById('navbar-sign-in')
421 let id_navbar_sign_in_cross = document.getElementById('navbar-sign-in-cross')
422 let id_navbar_sign_in_email = document.getElementById('navbar-sign-in-email')
423 let id_navbar_sign_in_form = document.getElementById('navbar-sign-in-form')
424 let id_navbar_sign_in_icon = document.getElementById('navbar-sign-in-icon')
425 let id_navbar_sign_in_message = document.getElementById('navbar-sign-in-message')
426 let id_navbar_sign_in_modal = document.getElementById('navbar-sign-in-modal')
427 let id_navbar_sign_in_password = document.getElementById('navbar-sign-in-password')
428 let id_navbar_sign_in_sign_in = document.getElementById('navbar-sign-in-sign-in')
429 let id_navbar_sign_in_spinner = document.getElementById('navbar-sign-in-spinner')
430 let id_navbar_sign_in_tick = document.getElementById('navbar-sign-in-tick')
431 let id_navbar_sign_out = document.getElementById('navbar-sign-out')
432 let id_navbar_sign_up = document.getElementById('navbar-sign-up')
433 let id_navbar_signed_in_status = document.getElementById('navbar-signed-in-status')
434 //let id_navbarDropdown = document.getElementById('navbarDropdown')
435 //let id_navbarSupportedContent = document.getElementById('navbarSupportedContent')
438 id_navbar_sign_in.addEventListener(
441 id_navbar_sign_in_email.value = ''
442 id_navbar_sign_in_password.value = ''
443 id_navbar_sign_in_sign_in.disabled = true
444 $('#navbar-sign-in-modal').modal('show')
448 $('#navbar-sign-in-modal').on(
450 () => {id_navbar_sign_in_email.focus()}
453 let sign_in_edited = () => {
454 id_navbar_sign_in_sign_in.disabled =
455 id_navbar_sign_in_email.value.length === 0 &&
456 id_navbar_sign_in_password.value.length === 0
457 id_navbar_sign_in_icon.hidden = false
458 //id_navbar_sign_in_tick.hidden = true
459 id_navbar_sign_in_cross.hidden = true
460 id_navbar_sign_in_spinner.hidden = true
461 id_navbar_sign_in_message.hidden = true
464 id_navbar_sign_in_email.addEventListener(
468 id_navbar_sign_in_password.addEventListener(
473 id_navbar_sign_in_sign_in.addEventListener(
476 id_navbar_sign_in_icon.hidden = false
477 //id_navbar_sign_in_tick.hidden = true
478 id_navbar_sign_in_cross.hidden = true
479 id_navbar_sign_in_spinner.hidden = true
480 // the below causes an ugly flicker, so just keep the message
481 //id_navbar_sign_in_message.hidden = true
483 if (!id_navbar_sign_in_form.checkValidity()) {
484 id_navbar_sign_in_form.classList.add('was-validated');
486 id_navbar_sign_in_icon.hidden = true
487 id_navbar_sign_in_cross.hidden = false
490 id_navbar_sign_in_form.classList.remove('was-validated');
492 let email = id_navbar_sign_in_email.value.slice(0, 256).toLowerCase()
494 id_navbar_sign_in_icon.hidden = true
495 id_navbar_sign_in_spinner.hidden = false
498 '/api/account/sign_in.json',
500 id_navbar_sign_in_password.value.slice(0, 256)
504 let problem = Problem.from(error)
506 if (problem.title === 'Email not yet verified') {
507 location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
511 id_navbar_sign_in_cross.hidden = false
512 id_navbar_sign_in_spinner.hidden = true
514 id_navbar_sign_in_message.textContent = problem.detail
515 //id_navbar_sign_in_message.classList.remove('text-success')
516 id_navbar_sign_in_message.classList.add('text-danger')
517 id_navbar_sign_in_message.hidden = false
520 //id_navbar_sign_in_tick.hidden = false
521 //id_navbar_sign_in_spinner.hidden = true
522 //id_navbar_sign_in_message.textContent = `You are now signed in as "${email}".`
523 //id_navbar_sign_in_message.classList.add('text-success')
524 //id_navbar_sign_in_message.classList.remove('text-danger')
525 //id_navbar_sign_in_message.hidden = false
527 id_navbar_signed_in_status.textContent = 'Signed in.' //`Signed in as ${email}.`
528 id_navbar_sign_in.hidden = true
529 id_navbar_sign_up.hidden = true
530 id_navbar_sign_out.hidden = false
533 id_navbar_sign_in_icon.hidden = false
534 id_navbar_sign_in_spinner.hidden = true
535 id_navbar_sign_in_message.hidden = true
536 id_navbar_message_modal_message.textContent = `You are now signed in as "${email}".`
537 $('#navbar-sign-in-modal').modal('hide')
538 $('#navbar-message-modal').modal('show')
543 id_navbar_sign_out.addEventListener(
548 '/api/account/sign_out.json'
552 let problem = Problem.from(error)
554 id_navbar_message_modal_message.textContent = problem.detail
555 $('#navbar-sign-in-modal').modal('hide')
556 $('#navbar-message-modal').modal('show')
560 id_navbar_signed_in_status.textContent = 'Browsing as guest.'
561 id_navbar_sign_in.hidden = false
562 id_navbar_sign_up.hidden = false
563 id_navbar_sign_out.hidden = true
566 id_navbar_message_modal_message.textContent = `You are now signed out.`
567 $('#navbar-sign-in-modal').modal('hide')
568 $('#navbar-message-modal').modal('show')
573 id_navbar_give_feedback.addEventListener(
576 // hack to move cursor to end of textarea
577 let temp = id_navbar_feedback_message1.value
578 id_navbar_feedback_message1.value = ''
579 id_navbar_feedback_message1.value = temp
581 $('#navbar-feedback-modal').modal('show')
586 $('#navbar-feedback-modal').on(
588 () => {id_navbar_feedback_message1.focus()}
591 let feedback_input_semaphore = new BinarySemaphore(false)
595 await feedback_input_semaphore.acquire()
596 await new Promise(resolve => setTimeout(resolve, 3000))
597 feedback_input_semaphore.try_acquire()
599 '/api/feedback/set_draft.json',
600 id_navbar_feedback_message1.value.length === 0 ?
603 message: id_navbar_feedback_message1.value.slice(0, 65536)
608 )() // ignore returned promise (start thread)
610 let feedback_edited = () => {
611 feedback_input_semaphore.release()
613 id_navbar_feedback_send_message.disabled =
614 id_navbar_feedback_message1.value.length === 0
615 id_navbar_feedback_icon.hidden = false
616 //id_navbar_feedback_tick.hidden = true
617 id_navbar_feedback_cross.hidden = true
618 id_navbar_feedback_spinner.hidden = true
619 id_navbar_feedback_message.hidden = true
622 id_navbar_feedback_message1.addEventListener(
627 id_navbar_feedback_send_message.addEventListener(
630 id_navbar_feedback_icon.hidden = false
631 //id_navbar_feedback_tick.hidden = true
632 id_navbar_feedback_cross.hidden = true
633 id_navbar_feedback_spinner.hidden = true
634 // the below causes an ugly flicker, so just keep the message
635 //id_navbar_feedback_message.hidden = true
637 if (!id_navbar_feedback_form.checkValidity()) {
638 id_navbar_feedback_form.classList.add('was-validated');
640 id_navbar_feedback_icon.hidden = true
641 id_navbar_feedback_cross.hidden = false
644 id_navbar_feedback_form.classList.remove('was-validated');
646 id_navbar_feedback_icon.hidden = true
647 id_navbar_feedback_spinner.hidden = false
650 '/api/feedback/send_message.json',
652 id_navbar_feedback_message1.value.slice(0, 65536)
656 let problem = Problem.from(error)
658 id_navbar_feedback_cross.hidden = false
659 id_navbar_feedback_spinner.hidden = true
661 id_navbar_feedback_message.textContent = problem.detail
662 //id_navbar_feedback_message.classList.remove('text-success')
663 id_navbar_feedback_message.classList.add('text-danger')
664 id_navbar_feedback_message.hidden = false
667 //id_navbar_feedback_tick.hidden = false
668 //id_navbar_feedback_spinner.hidden = true
669 //id_navbar_feedback_message.textContent = 'We have received your message. We will be in touch as soon as possible.'
670 //id_navbar_feedback_message.classList.add('text-success')
671 //id_navbar_feedback_message.classList.remove('text-danger')
672 //id_navbar_feedback_message.hidden = false
674 id_navbar_feedback_icon.hidden = false
675 id_navbar_feedback_spinner.hidden = true
676 id_navbar_feedback_message.hidden = true
677 id_navbar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
678 $('#navbar-feedback-modal').modal('hide')
679 $('#navbar-message-modal').modal('show')