1 let assert = require('assert')
2 let jst_server = (await import('@ndcode/jst_server')).default
3 let XDate = require('xdate')
7 a.length === b.length && a.every((value, index) => value === b[index])
9 return async (env, head, body, scripts) => {
10 //let cart = await _require('/online_store/cart.jst')
11 let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
12 let fa_times_circle = await env.site.get_min_svg('/_svg/fa_times-circle.svg')
13 let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
14 let fa_unlock_alt = await env.site.get_min_svg('/_svg/fa_unlock-alt.svg')
15 let fa_search = await env.site.get_min_svg('/_svg/fa_search.svg')
16 let get_session = await _require('/_lib/get_session.jst')
17 //let icon_cart_small = await env.site.get_min_svg('/_svg/icon_cart_small.svg')
18 let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
19 //let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
20 let logo_large = await env.site.get_min_svg('/_svg/logo_large.svg')
21 let page = await _require('/_lib/page.jst')
23 // initialize env.cart
26 // compute breadcrumbs from directories of the path
27 let component_names = env.parsed_url.pathname.split('/')
28 assert(component_names.length >= 2)
29 assert(component_names[0].length === 0)
30 assert(component_names[component_names.length - 1].length)
31 component_names = component_names.slice(1, -1)
33 let transaction = await env.site.database.Transaction()
35 let site_title, copyright
36 let component_titles // collects breadcrumb titles for current page
37 let menu_names, menu_titles // collects top level of menu for the navbar
40 let root = await transaction.get()
42 let session = await get_session(env, root)
43 signed_in_as = await session.get_json('signed_in_as')
45 let globals = await root.get('globals')
46 site_title = await globals.get_json('site_title')
47 copyright = await globals.get_json('copyright')
49 let navigation = await root.get('navigation')
50 if (navigation === undefined)
51 throw new jst_server.Problem(
53 'Please import the navigation tree into the database.',
57 // this code is taken from get_navigation.jst and instrumented
59 component_titles = [await p.get_json('title')] // Home
60 for (let i = 0; i < component_names.length; ++i) {
61 let children = await p.get('children')
62 p = await children.get(component_names[i])
63 if (navigation === undefined)
64 throw new jst_server.Problem(
66 `Can't find the path "${
67 component_names.slice(0, i + 1).map(name => '/' + name).join('')
68 }" in the navigation tree.`,
71 component_titles.push(await p.get_json('title'))
74 // similar to above but walks the top level laterally (not deeply)
75 menu_names = await navigation.get_json('menu')
76 let children = await navigation.get('children')
77 menu_titles = [await navigation.get_json('title')] // Home
78 for (let i = 0; i < menu_names.length; ++i) {
79 let child = await children.get(menu_names[i])
80 if (child === undefined)
81 throw new jst_server.Problem(
83 `Can't find the path "/${menu_names[i]}" in the navigation tree.`
86 menu_titles.push(await child.get('title'))
89 feedback_draft = await session.get_json('feedback_draft')
90 if (feedback_draft === undefined || env.now >= feedback_draft.expires)
94 transaction.rollback()
97 // save breadcrumbs and their titles for breadcrumbs.jst
98 // note: component_titles.length === component_names.length + 1
99 // component_titles[0] corresponds to /, is 'Home' or similar
100 // component_titles[i] corresponds to component_names[i - 1], i >= 1
101 env.component_names = component_names
102 env.component_titles = component_titles
103 console.log('cn', component_names)
104 console.log('ct', component_titles)
106 // note: menu_titles.length === menu_names.length + 1
107 // menu_titles[0] corresponds to /, is 'Home' or similar
108 // menu_titles[i] corresponds to menu_names[i - 1], i >= 1a
109 // (navbar has Home appearing at same level as its immediate children)
110 console.log('mn', menu_names)
111 console.log('mt', menu_titles)
120 component_names.length >= 2 ? 1 : component_names.length
129 // extract top-level directory name
130 assert(env.parsed_url.pathname.slice(0, 1) === '/')
131 let index = env.parsed_url.pathname.indexOf('/', 1)
132 let dir = index === -1 ? '' : env.parsed_url.pathname.slice(1, index)
136 div.row.align-items-center.py-3 {
138 _out.push(logo_large)
141 div.'mb-1'.text-right {
142 span#navbar-signed-in-status {
143 if (signed_in_as !== undefined)
144 'Signed in.' //`Signed in as ${signed_in_as}.`
149 if (signed_in_as !== undefined)
150 a#navbar-sign-in(href="#" hidden) {'Sign in'}
152 a#navbar-sign-in(href="#") {'Sign in'}
154 if (signed_in_as !== undefined)
155 a#navbar-sign-up(href="/my_account/sign_up/index.html" hidden) {'Sign up'}
157 a#navbar-sign-up(href="/my_account/sign_up/index.html") {'Sign up'}
159 if (signed_in_as !== undefined)
160 a#navbar-sign-out(href="#") {'Sign out'}
162 a#navbar-sign-out(href="#" hidden) {'Sign out'}
165 form(action="/search/index.html") {
167 input.form-control(name="query" type="text" placeholder="Search" aria-describedby="search-button") {}
168 div.input-group-append {
169 button.btn.btn-outline-secondary#navbar-search-button(type="submit") {
171 div.icon24-inner {_out.push(fa_search)}
179 //div.'col-sm-1'.vbottom {
180 // // a nested div is used to avoid hover colour on the padding
181 // div.nav-li-a(style="text-align: center;") {
182 // a(href="/online_store/view_cart/index.html") {
184 // _out.push(icon_cart_small)
188 // `${(env.cart.items || []).length}`
197 nav.navbar.navbar-expand-lg.navbar-dark.bg-primary.scrollbar-fix {
199 //a.navbar-brand(href="#") {'Navbar'}
201 button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation") {
202 span.navbar-toggler-icon {}
204 div.collapse.navbar-collapse#navbarSupportedContent {
205 ul.navbar-nav.mr-auto {
206 // the active entry in the navbar bar is based on which top-level
207 // page we are under, even if we are not directly on that page
208 // but one of its children, this may be unexpected as the active
209 // entry does not highlight on hover, but you can still click it;
210 // we determine here the path to the corresponding top-level page
211 let component_prefix = component_names.slice(0, 1)
213 for (let i = 0; i < menu_titles.length; ++i) {
214 // construct path to the top-level page about to be described
216 i == 0 ? [] : [menu_names[i - 1]]
217 let menu_prefix_path =
218 menu_prefix.map(name => '/' + name).join('') + '/index.html'
220 if (arrays_equal(menu_prefix, component_prefix))
222 a.nav-link(href=menu_prefix_path) {
224 span.sr-only {' (current)'}
229 a.nav-link(href=menu_prefix_path) {
234 //li.nav-item.dropdown {
235 // a.nav-link.dropdown-toggle#navbarDropdown(href="#" role="button" data-toggle="dropdown" aria-expanded="false") {
238 // div.dropdown-menu(aria-labelledby="navbarDropdown") {
239 // a.dropdown-item(href="#") {
243 // a.dropdown-item(href="#") {
246 // div.dropdown-divider {}
247 // a.dropdown-item(href="#") {
248 // 'Something else here'
253 // a.nav-link.disabled {
258 ul.navbar-nav.ml-auto {
260 a.nav-link#navbar-give-feedback(href="#") {'Give feedback'}
271 footer.scrollbar-fix {
273 a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
274 img(alt="Creative Commons License" style="border-width:0;" src="/images/by-sa_3.0_88x31.png") {}
278 a(href="https://git.ndcode.org/public/ndcode_site.git") {
281 ' and licensed under a '
282 a(rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/") {
283 'Creative Commons Attribution-ShareAlike 3.0 Unported License'
288 p {'Example code fragments embedded within the text are placed in the public domain unless otherwise noted.'}
290 p {`Copyright © ${new XDate(env.now).getUTCFullYear()} ${copyright}.`}
295 div.modal#navbar-sign-in-modal(tabindex="-1") {
299 span.h4.modal-title {'Sign in'}
302 form#navbar-sign-in-form {
306 label.form-label(for="navbar-sign-in-email") {'Email'}
307 input.form-control#navbar-sign-in-email(type="email" required maxlength=256) {}
308 div.invalid-feedback {'Please enter your account\'s email address.'}
315 label.form-label(for="navbar-sign-in-password") {'Password'}
316 input.form-control#navbar-sign-in-password(type="password" required minlength=8 maxlength=256) {}
317 div.invalid-feedback {'Please enter at least 8 characters.'}
325 a(href="/my_account/sign_up/index.html") {'Sign up'}
330 a(href="/my_account/password_reset/index.html") {'Password reset'}
333 p.'mt-3'.mb-0#navbar-sign-in-message(hidden) {}
336 button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
337 div.icon24-outer.mr-2 {
338 div.icon24-inner {_out.push(fa_arrow_circle_left)}
342 button.btn.btn-primary#navbar-sign-in-sign-in(type="button") {
343 div.icon24-outer.mr-2#navbar-sign-in-icon {
344 div.icon24-inner {_out.push(fa_unlock_alt)}
346 //div.icon24-outer.mr-2#navbar-sign-in-tick(hidden) {
347 // div.icon24-inner {_out.push(icon_tick)}
349 div.icon24-outer.mr-2#navbar-sign-in-cross(hidden) {
350 div.icon24-inner {_out.push(icon_cross)}
352 div.icon24-outer.mr-2#navbar-sign-in-spinner(hidden) {
354 div.spinner-border.spinner-border-sm(role="status") {}
364 div.modal#navbar-feedback-modal(tabindex="-1") {
368 span.h4.modal-title {'Give feedback'}
372 'Did you notice something not quite right, or just want to share your impression of this page?'
375 form#navbar-feedback-form {
379 label.form-label(for="navbar-feedback-message1") {'Message'}
380 textarea.form-control#navbar-feedback-message1(placeholder="I noticed that..." required rows=4 maxlength=65536) {
382 `${feedback_draft.message}`
384 div.invalid-feedback {'Please let us have your thoughts.'}
390 p.'mt-3'.mb-0#navbar-feedback-message(hidden) {}
393 button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
394 div.icon24-outer.mr-2 {
395 div.icon24-inner {_out.push(fa_arrow_circle_left)}
400 button.btn.btn-primary#navbar-feedback-send-message(type="button") {
401 div.icon24-outer.mr-2#navbar-feedback-icon {
402 div.icon24-inner {_out.push(fa_envelope)}
404 //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
405 // div.icon24-inner {_out.push(icon_tick)}
407 div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
408 div.icon24-inner {_out.push(icon_cross)}
410 div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
412 div.spinner-border.spinner-border-sm(role="status") {}
418 button.btn.btn-primary#navbar-feedback-send-message(type="button" disabled) {
419 div.icon24-outer.mr-2#navbar-feedback-icon {
420 div.icon24-inner {_out.push(fa_envelope)}
422 //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
423 // div.icon24-inner {_out.push(icon_tick)}
425 div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
426 div.icon24-inner {_out.push(icon_cross)}
428 div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
430 div.spinner-border.spinner-border-sm(role="status") {}
440 div.modal#navbar-message-modal(tabindex="-1") {
444 span.h4.modal-title {'Message'}
446 div.modal-body#navbar-message-modal-message {
449 button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
450 div.icon24-outer.mr-2 {
451 div.icon24-inner {_out.push(fa_times_circle)}
462 script(src="/js/utils.js") {}
465 // this function can be overridden in a further script
466 function sign_in_out(status) {
470 document.addEventListener(
473 let id_navbar_feedback_cross = document.getElementById('navbar-feedback-cross')
474 let id_navbar_feedback_form = document.getElementById('navbar-feedback-form')
475 let id_navbar_feedback_icon = document.getElementById('navbar-feedback-icon')
476 let id_navbar_feedback_message = document.getElementById('navbar-feedback-message')
477 let id_navbar_feedback_message1 = document.getElementById('navbar-feedback-message1')
478 let id_navbar_feedback_modal = document.getElementById('navbar-feedback-modal')
479 let id_navbar_feedback_send_message = document.getElementById('navbar-feedback-send-message')
480 let id_navbar_feedback_spinner = document.getElementById('navbar-feedback-spinner')
481 let id_navbar_feedback_tick = document.getElementById('navbar-feedback-tick')
482 let id_navbar_give_feedback = document.getElementById('navbar-give-feedback')
483 let id_navbar_message_modal = document.getElementById('navbar-message-modal')
484 let id_navbar_message_modal_message = document.getElementById('navbar-message-modal-message')
485 let id_navbar_search_button = document.getElementById('navbar-search-button')
486 let id_navbar_sign_in = document.getElementById('navbar-sign-in')
487 let id_navbar_sign_in_cross = document.getElementById('navbar-sign-in-cross')
488 let id_navbar_sign_in_email = document.getElementById('navbar-sign-in-email')
489 let id_navbar_sign_in_form = document.getElementById('navbar-sign-in-form')
490 let id_navbar_sign_in_icon = document.getElementById('navbar-sign-in-icon')
491 let id_navbar_sign_in_message = document.getElementById('navbar-sign-in-message')
492 let id_navbar_sign_in_modal = document.getElementById('navbar-sign-in-modal')
493 let id_navbar_sign_in_password = document.getElementById('navbar-sign-in-password')
494 let id_navbar_sign_in_sign_in = document.getElementById('navbar-sign-in-sign-in')
495 let id_navbar_sign_in_spinner = document.getElementById('navbar-sign-in-spinner')
496 let id_navbar_sign_in_tick = document.getElementById('navbar-sign-in-tick')
497 let id_navbar_sign_out = document.getElementById('navbar-sign-out')
498 let id_navbar_sign_up = document.getElementById('navbar-sign-up')
499 let id_navbar_signed_in_status = document.getElementById('navbar-signed-in-status')
500 //let id_navbarDropdown = document.getElementById('navbarDropdown')
501 //let id_navbarSupportedContent = document.getElementById('navbarSupportedContent')
504 id_navbar_sign_in.addEventListener(
507 id_navbar_sign_in_email.value = ''
508 id_navbar_sign_in_password.value = ''
509 id_navbar_sign_in_sign_in.disabled = true
510 $('#navbar-sign-in-modal').modal('show')
514 $('#navbar-sign-in-modal').on(
516 () => {id_navbar_sign_in_email.focus()}
519 let sign_in_edited = () => {
520 id_navbar_sign_in_sign_in.disabled =
521 id_navbar_sign_in_email.value.length === 0 &&
522 id_navbar_sign_in_password.value.length === 0
523 id_navbar_sign_in_icon.hidden = false
524 //id_navbar_sign_in_tick.hidden = true
525 id_navbar_sign_in_cross.hidden = true
526 id_navbar_sign_in_spinner.hidden = true
527 id_navbar_sign_in_message.hidden = true
530 id_navbar_sign_in_email.addEventListener(
534 id_navbar_sign_in_password.addEventListener(
539 id_navbar_sign_in_sign_in.addEventListener(
542 id_navbar_sign_in_icon.hidden = false
543 //id_navbar_sign_in_tick.hidden = true
544 id_navbar_sign_in_cross.hidden = true
545 id_navbar_sign_in_spinner.hidden = true
546 // the below causes an ugly flicker, so just keep the message
547 //id_navbar_sign_in_message.hidden = true
549 if (!id_navbar_sign_in_form.checkValidity()) {
550 id_navbar_sign_in_form.classList.add('was-validated');
552 id_navbar_sign_in_icon.hidden = true
553 id_navbar_sign_in_cross.hidden = false
556 id_navbar_sign_in_form.classList.remove('was-validated');
558 let email = id_navbar_sign_in_email.value.slice(0, 256).toLowerCase()
560 id_navbar_sign_in_icon.hidden = true
561 id_navbar_sign_in_spinner.hidden = false
564 '/api/account/sign_in.json',
566 id_navbar_sign_in_password.value.slice(0, 256)
570 let problem = Problem.from(error)
572 if (problem.title === 'Email not yet verified') {
573 location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
577 id_navbar_sign_in_cross.hidden = false
578 id_navbar_sign_in_spinner.hidden = true
580 id_navbar_sign_in_message.textContent = problem.detail
581 //id_navbar_sign_in_message.classList.remove('text-success')
582 id_navbar_sign_in_message.classList.add('text-danger')
583 id_navbar_sign_in_message.hidden = false
586 //id_navbar_sign_in_tick.hidden = false
587 //id_navbar_sign_in_spinner.hidden = true
588 //id_navbar_sign_in_message.textContent = `You are now signed in as "${email}".`
589 //id_navbar_sign_in_message.classList.add('text-success')
590 //id_navbar_sign_in_message.classList.remove('text-danger')
591 //id_navbar_sign_in_message.hidden = false
593 if (sign_in_out(true))
594 // if location has been changed, leave the spinner and do
595 // not show status/dialog, as it causes an annoying flicker
598 id_navbar_signed_in_status.textContent = 'Signed in.' //`Signed in as ${email}.`
599 id_navbar_sign_in.hidden = true
600 id_navbar_sign_up.hidden = true
601 id_navbar_sign_out.hidden = false
603 id_navbar_sign_in_icon.hidden = false
604 id_navbar_sign_in_spinner.hidden = true
605 id_navbar_sign_in_message.hidden = true
606 id_navbar_message_modal_message.textContent = `You are now signed in as "${email}".`
607 $('#navbar-sign-in-modal').modal('hide')
608 $('#navbar-message-modal').modal('show')
613 id_navbar_sign_out.addEventListener(
618 '/api/account/sign_out.json'
622 let problem = Problem.from(error)
624 id_navbar_message_modal_message.textContent = problem.detail
625 $('#navbar-sign-in-modal').modal('hide')
626 $('#navbar-message-modal').modal('show')
630 if (sign_in_out(false))
631 // if location has been changed, leave the spinner and do
632 // not show status/dialog, as it causes an annoying flicker
635 id_navbar_signed_in_status.textContent = 'Browsing as guest.'
636 id_navbar_sign_in.hidden = false
637 id_navbar_sign_up.hidden = false
638 id_navbar_sign_out.hidden = true
640 id_navbar_message_modal_message.textContent = `You are now signed out.`
641 $('#navbar-sign-in-modal').modal('hide')
642 $('#navbar-message-modal').modal('show')
647 id_navbar_give_feedback.addEventListener(
650 // hack to move cursor to end of textarea
651 let temp = id_navbar_feedback_message1.value
652 id_navbar_feedback_message1.value = ''
653 id_navbar_feedback_message1.value = temp
655 $('#navbar-feedback-modal').modal('show')
660 $('#navbar-feedback-modal').on(
662 () => {id_navbar_feedback_message1.focus()}
665 let feedback_input_semaphore = new BinarySemaphore(false)
669 await feedback_input_semaphore.acquire()
670 await new Promise(resolve => setTimeout(resolve, 3000))
671 feedback_input_semaphore.try_acquire()
673 '/api/feedback/set_draft.json',
674 id_navbar_feedback_message1.value.length === 0 ?
677 message: id_navbar_feedback_message1.value.slice(0, 65536)
682 )() // ignore returned promise (start thread)
684 let feedback_edited = () => {
685 feedback_input_semaphore.release()
687 id_navbar_feedback_send_message.disabled =
688 id_navbar_feedback_message1.value.length === 0
689 id_navbar_feedback_icon.hidden = false
690 //id_navbar_feedback_tick.hidden = true
691 id_navbar_feedback_cross.hidden = true
692 id_navbar_feedback_spinner.hidden = true
693 id_navbar_feedback_message.hidden = true
696 id_navbar_feedback_message1.addEventListener(
701 id_navbar_feedback_send_message.addEventListener(
704 id_navbar_feedback_icon.hidden = false
705 //id_navbar_feedback_tick.hidden = true
706 id_navbar_feedback_cross.hidden = true
707 id_navbar_feedback_spinner.hidden = true
708 // the below causes an ugly flicker, so just keep the message
709 //id_navbar_feedback_message.hidden = true
711 if (!id_navbar_feedback_form.checkValidity()) {
712 id_navbar_feedback_form.classList.add('was-validated');
714 id_navbar_feedback_icon.hidden = true
715 id_navbar_feedback_cross.hidden = false
718 id_navbar_feedback_form.classList.remove('was-validated');
720 id_navbar_feedback_icon.hidden = true
721 id_navbar_feedback_spinner.hidden = false
724 '/api/feedback/send_message.json',
726 id_navbar_feedback_message1.value.slice(0, 65536)
730 let problem = Problem.from(error)
732 id_navbar_feedback_cross.hidden = false
733 id_navbar_feedback_spinner.hidden = true
735 id_navbar_feedback_message.textContent = problem.detail
736 //id_navbar_feedback_message.classList.remove('text-success')
737 id_navbar_feedback_message.classList.add('text-danger')
738 id_navbar_feedback_message.hidden = false
741 //id_navbar_feedback_tick.hidden = false
742 //id_navbar_feedback_spinner.hidden = true
743 //id_navbar_feedback_message.textContent = 'We have received your message. We will be in touch as soon as possible.'
744 //id_navbar_feedback_message.classList.add('text-success')
745 //id_navbar_feedback_message.classList.remove('text-danger')
746 //id_navbar_feedback_message.hidden = false
748 id_navbar_feedback_icon.hidden = false
749 id_navbar_feedback_spinner.hidden = true
750 id_navbar_feedback_message.hidden = true
751 id_navbar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
752 $('#navbar-feedback-modal').modal('hide')
753 $('#navbar-message-modal').modal('show')