da759f9f6a259fa059da463b44c2e3fccf1e5e75
[ndcode_site.git] / _lib / navbar.jst
1 let assert = require('assert')
2 let XDate = require('xdate')
3
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')
18
19   // initialize env.cart
20   //await cart(env)
21
22   let transaction = await env.site.database.Transaction()
23   let signed_in_as
24   let site_title, copyright
25   let feedback_draft
26   try {
27     let root = await transaction.get({})
28
29     let session = await get_session(env, root)
30     signed_in_as = await session.get_json('signed_in_as')
31
32     let globals = await root.get('globals', {})
33     site_title = await globals.get_json('site_title')
34     copyright = await globals.get_json('copyright')
35
36     feedback_draft = await session.get_json('feedback_draft')
37     if (feedback_draft === undefined || env.now >= feedback_draft.expires)
38       feedback_draft = null
39   }
40   finally {
41     transaction.rollback()
42   }
43
44   await page(
45     env,
46     // head
47     async _out => {
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)
52
53       title {
54         `${site_title}: ${
55           dir.length === 0 ?
56             'Home' :
57             menu.entries[menu.index[dir]].name
58           }`
59       }
60
61       await head(_out)
62     },
63     // body
64     async _out => {
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)
69
70       div.scrollbar-fix {
71         div.container {
72           div.row.align-items-center.py-3 {
73             div.col-sm-8 {
74               _out.push(logo_large)
75             }
76             div.'col-sm-4' {
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}.`
81                   else
82                     'Browsing as guest.'
83                 }
84                 ' '
85                 if (signed_in_as !== undefined)
86                   a#navbar-sign-in(href="#" hidden) {'Sign in'}
87                 else
88                   a#navbar-sign-in(href="#") {'Sign in'}
89                 ' '
90                 if (signed_in_as !== undefined)
91                   a#navbar-sign-up(href="/my_account/sign_up/index.html" hidden) {'Sign up'}
92                 else
93                   a#navbar-sign-up(href="/my_account/sign_up/index.html") {'Sign up'}
94                 ' '
95                 if (signed_in_as !== undefined)
96                   a#navbar-sign-out(href="#") {'Sign out'}
97                 else
98                   a#navbar-sign-out(href="#" hidden) {'Sign out'}
99               }
100
101               form(action="/search/index.html") {
102                 div.input-group {
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") {
106                       div.icon24-outer {
107                         div.icon24-inner {_out.push(fa_search)}
108                       }
109                     }
110                   }
111                 }
112               }
113             }
114
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") {
119             //      div.cart-icon {
120             //        _out.push(icon_cart_small)
121             //      }
122             //      div.cart-number {
123             //        div.cart-circle {
124             //          `${(env.cart.items || []).length}`
125             //        }
126             //      }
127             //    }
128             //  }
129             //}
130           }
131         }
132       }
133       nav.navbar.navbar-expand-lg.navbar-dark.bg-primary.scrollbar-fix {
134         div.container {
135           //a.navbar-brand(href="#") {'Navbar'}
136           //' '
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 {}
139           }
140           div.collapse.navbar-collapse#navbarSupportedContent {
141             ul.navbar-nav.mr-auto {
142               if (dir.length === 0)
143                 li.nav-item.active {
144                   a.nav-link(href="/index.html") {
145                     'Home'
146                     span.sr-only {' (current)'}
147                   }
148                 }
149               else
150                 li.nav-item {
151                   a.nav-link(href="/index.html") {'Home'}
152                 }
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)
157                     li.nav-item.active {
158                       a.nav-link(href=`/${entries[i].dir}/index.html`) {
159                         `${entries[i].name}`
160                         span.sr-only {' (current)'}
161                       }
162                     }
163                   else
164                     li.nav-item {
165                       a.nav-link(href=`/${entries[i].dir}/index.html`) {
166                         `${entries[i].name}`
167                       }
168                     }
169               //li.nav-item.dropdown {
170               //  a.nav-link.dropdown-toggle#navbarDropdown(href="#" role="button" data-toggle="dropdown" aria-expanded="false") {
171               //    'Dropdown'
172               //  }
173               //  div.dropdown-menu(aria-labelledby="navbarDropdown") {
174               //    a.dropdown-item(href="#") {
175               //      'Action'
176               //    }
177               //    ' '
178               //    a.dropdown-item(href="#") {
179               //      'Another action'
180               //    }
181               //    div.dropdown-divider {}
182               //    a.dropdown-item(href="#") {
183               //      'Something else here'
184               //    }
185               //  }
186               //}
187               //li.nav-item {
188               //  a.nav-link.disabled {
189               //    'Disabled'
190               //  }
191               //}
192             }
193             ul.navbar-nav.ml-auto {
194               li.nav-item {
195                 a.nav-link#navbar-give-feedback(href="#") {'Give feedback'}
196               }
197             }
198           }
199         }
200       }
201       div.scrollbar-fix {
202         div.container {
203           await body(_out)
204         }
205       }
206       footer.scrollbar-fix {
207         div.container {
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") {}
210           }
211           p {
212             'This website is '
213             a(href="https://git.ndcode.org/public/ndcode_site.git") {
214               'open source'
215             }
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'
219             }
220             '.'
221           }
222
223           p {'Example code fragments embedded within the text are placed in the public domain unless otherwise noted.'}
224
225           p {`Copyright © ${new XDate(env.now).getUTCFullYear()} ${copyright}.`}
226         }
227       }
228
229       // hidden part
230       div#navbar-sign-in-modal.modal.fade(role="dialog") {
231         div.modal-dialog {
232           div.modal-content {
233             div.modal-header {
234               span.h4.modal-title {'Sign in'}
235             }
236             div.modal-body {
237               div.row {
238                 div.col-md-12 {
239                   div.form-group {
240                     label.form-label(for="navbar-sign-in-email") {'Email'}
241                     input.form-control#navbar-sign-in-email(type="text" placeholder="Account email address" required maxlength=256) {}
242                   }
243                 }
244               }
245               div.row {
246                 div.col-md-12 {
247                   div.form-group {
248                     label.form-label(for="navbar-sign-in-password") {'Password'}
249                     input.form-control#navbar-sign-in-password(type="password" placeholder="Account password" required minlength=8 maxlength=256) {}
250                   }
251                 }
252               }
253
254               p.mt-2 {
255                 'No account yet? '
256                 a(href="/my_account/sign_up/index.html") {'Sign up'}
257               }
258
259               p {
260                 'Forgot password? '
261                 a(href="/my_account/password_reset/index.html") {'Password reset'}
262               }
263             }
264             div.modal-footer {
265               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
266                 div.icon24-outer.mr-2 {
267                   div.icon24-inner {_out.push(fa_arrow_circle_left)}
268                 }
269                 'Back'
270               }
271               button.btn.btn-primary#navbar-sign-in-submit(type="button") {
272                 div.icon24-outer.mr-2 {
273                   div.icon24-inner {_out.push(fa_unlock_alt)}
274                 }
275                 'Sign in'
276               }
277             }
278           }
279         }
280       }
281
282       div#navbar-feedback-modal.modal.fade(role="dialog") {
283         div.modal-dialog {
284           div.modal-content {
285             div.modal-header {
286               span.h4.modal-title {'Give feedback'}
287             }
288             div.modal-body {
289               p {
290                 'Did you notice something not quite right, or just want to share your impression of this page?'
291               }
292
293               form#navbar-feedback-form {
294                 div.row {
295                   div.col-md-12 {
296                     div.form-group {
297                       label.form-label(for="navbar-feedback-message1") {'Message'}
298                       textarea.form-control#navbar-feedback-message1(placeholder="I noticed that..." required rows=4 maxlength=65536) {
299                         if (feedback_draft)
300                           `${feedback_draft.message}`
301                       }
302                       div.invalid-feedback {'Please let us have your thoughts.'}
303                     }
304                   }
305                 }
306               }
307
308               p.'mt-3'.mb-0#navbar-feedback-message(hidden) {}
309             }
310             div.modal-footer {
311               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
312                 div.icon24-outer.mr-2 {
313                   div.icon24-inner {_out.push(fa_arrow_circle_left)}
314                 }
315                 'Back'
316               }
317               if (feedback_draft)
318                 button.btn.btn-primary#navbar-feedback-send-message(type="button") {
319                   div.icon24-outer.mr-2#navbar-feedback-icon {
320                     div.icon24-inner {_out.push(fa_envelope)}
321                   }
322                   //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
323                   //  div.icon24-inner {_out.push(icon_tick)}
324                   //}
325                   div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
326                     div.icon24-inner {_out.push(icon_cross)}
327                   }
328                   div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
329                     div.icon24-inner {
330                       div.spinner-border.spinner-border-sm(role="status") {}
331                     }
332                   }
333                   'Send message'
334                 }
335               else
336                 button.btn.btn-primary#navbar-feedback-send-message(type="button" disabled) {
337                   div.icon24-outer.mr-2#navbar-feedback-icon {
338                     div.icon24-inner {_out.push(fa_envelope)}
339                   }
340                   //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
341                   //  div.icon24-inner {_out.push(icon_tick)}
342                   //}
343                   div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
344                     div.icon24-inner {_out.push(icon_cross)}
345                   }
346                   div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
347                     div.icon24-inner {
348                       div.spinner-border.spinner-border-sm(role="status") {}
349                     }
350                   }
351                   'Send message'
352                 }
353             }
354           }
355         }
356       }
357
358       div#navbar-message-modal.modal.fade(role="dialog") {
359         div.modal-dialog {
360           div.modal-content {
361             div.modal-header {
362               span.h4.modal-title {'Message'}
363             }
364             div.modal-body#navbar-message-modal-message {
365             }
366             div.modal-footer {
367               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
368                 div.icon24-outer.mr-2 {
369                   div.icon24-inner {_out.push(fa_times_circle)}
370                 }
371                 'Close'
372               }
373             }
374           }
375         }
376       }
377     },
378     // scripts
379     async _out => {
380       script(src="/js/utils.js") {}
381
382       script {
383         // this function can be overridden in a further script
384         function sign_in_out(status) {
385         }
386
387         document.addEventListener(
388           'DOMContentLoaded',
389           () => {
390             let id_navbar_feedback_cross = document.getElementById('navbar-feedback-cross')
391             let id_navbar_feedback_form = document.getElementById('navbar-feedback-form')
392             let id_navbar_feedback_icon = document.getElementById('navbar-feedback-icon')
393             let id_navbar_feedback_message = document.getElementById('navbar-feedback-message')
394             let id_navbar_feedback_message1 = document.getElementById('navbar-feedback-message1')
395             let id_navbar_feedback_modal = document.getElementById('navbar-feedback-modal')
396             let id_navbar_feedback_send_message = document.getElementById('navbar-feedback-send-message')
397             let id_navbar_feedback_spinner = document.getElementById('navbar-feedback-spinner')
398             //let id_navbar_feedback_tick = document.getElementById('navbar-feedback-tick')
399             let id_navbar_give_feedback = document.getElementById('navbar-give-feedback')
400             let id_navbar_message_modal = document.getElementById('navbar-message-modal')
401             let id_navbar_message_modal_message = document.getElementById('navbar-message-modal-message')
402             let id_navbar_search_button = document.getElementById('navbar-search-button')
403             let id_navbar_sign_in = document.getElementById('navbar-sign-in')
404             let id_navbar_sign_in_email = document.getElementById('navbar-sign-in-email')
405             let id_navbar_sign_in_modal = document.getElementById('navbar-sign-in-modal')
406             let id_navbar_sign_in_password = document.getElementById('navbar-sign-in-password')
407             let id_navbar_sign_in_submit = document.getElementById('navbar-sign-in-submit')
408             let id_navbar_sign_out = document.getElementById('navbar-sign-out')
409             let id_navbar_sign_up = document.getElementById('navbar-sign-up')
410             let id_navbar_signed_in_status = document.getElementById('navbar-signed-in-status')
411             //let id_navbarDropdown = document.getElementById('navbarDropdown')
412             //let id_navbarSupportedContent = document.getElementById('navbarSupportedContent')
413
414             // sign in form
415             id_navbar_sign_in.addEventListener(
416               'click',
417               () => {
418                 id_navbar_sign_in_email.value = ''
419                 id_navbar_sign_in_password.value = ''
420                 $('#navbar-sign-in-modal').modal('show')
421               }
422             )
423
424             $('#navbar-sign-in-modal').on(
425               'shown.bs.modal',
426               () => {id_navbar_sign_in_email.focus()}
427             )
428
429             id_navbar_sign_in_submit.addEventListener(
430               'click',
431               async () => {
432                 let email
433                 try {
434                   email = id_navbar_sign_in_email.value.slice(0, 256).toLowerCase()
435                   await api_call(
436                     '/api/account/sign_in.json',
437                     email,
438                     id_navbar_sign_in_password.value.slice(0, 256)
439                   )
440                 }
441                 catch (error) {
442                   let problem = Problem.from(error)
443
444                   if (problem.title === 'Email not yet verified') {
445                     location.href = `/my_account/send_verification_email?email=${encodeURIComponent(email)}`
446                     return
447                   }
448
449                   id_navbar_message_modal_message.textContent = problem.detail
450                   $('#navbar-sign-in-modal').modal('hide')
451                   $('#navbar-message-modal').modal('show')
452                   return
453                 }
454
455                 id_navbar_signed_in_status.textContent = 'Signed in.' //`Signed in as ${email}.`
456                 id_navbar_sign_in.hidden = true
457                 id_navbar_sign_up.hidden = true
458                 id_navbar_sign_out.hidden = false
459                 sign_in_out(true)
460
461                 id_navbar_message_modal_message.textContent = `You are now signed in as "${email}".`
462                 $('#navbar-sign-in-modal').modal('hide')
463                 $('#navbar-message-modal').modal('show')
464               }
465             )
466
467             // sign out button
468             id_navbar_sign_out.addEventListener(
469               'click',
470               async () => {
471                 try {
472                   await api_call(
473                     '/api/account/sign_out.json'
474                   )
475                 }
476                 catch (error) {
477                   let problem = Problem.from(error)
478
479                   id_navbar_message_modal_message.textContent = problem.detail
480                   $('#navbar-sign-in-modal').modal('hide')
481                   $('#navbar-message-modal').modal('show')
482                   return
483                 }
484
485                 id_navbar_signed_in_status.textContent = 'Browsing as guest.'
486                 id_navbar_sign_in.hidden = false
487                 id_navbar_sign_up.hidden = false
488                 id_navbar_sign_out.hidden = true
489                 sign_in_out(false)
490
491                 id_navbar_message_modal_message.textContent = `You are now signed out.`
492                 $('#navbar-sign-in-modal').modal('hide')
493                 $('#navbar-message-modal').modal('show')
494               }
495             )
496
497             // feedback form
498             id_navbar_give_feedback.addEventListener(
499               'click',
500               () => {
501                 // hack to move cursor to end of textarea
502                 let temp = id_navbar_feedback_message1.value
503                 id_navbar_feedback_message1.value = ''
504                 id_navbar_feedback_message1.value = temp
505
506                 $('#navbar-feedback-modal').modal('show')
507                 return false
508               }
509             )
510
511             $('#navbar-feedback-modal').on(
512               'shown.bs.modal',
513               () => {id_navbar_feedback_message1.focus()}
514             )
515
516             let feedback_input_semaphore = new BinarySemaphore(false)
517             ;(
518               async () => {
519                 while (true) {
520                   await feedback_input_semaphore.acquire()
521                   await new Promise(resolve => setTimeout(resolve, 3000))
522                   feedback_input_semaphore.try_acquire()
523                   await api_call(
524                     '/api/feedback/set_draft.json',
525                     id_navbar_feedback_message1.value.length === 0 ?
526                       null :
527                       {
528                         message: id_navbar_feedback_message1.value.slice(0, 65536)
529                       }
530                   )
531                 }
532               }
533             )() // ignore returned promise (start thread)
534
535             let feedback_edited = () => {
536               feedback_input_semaphore.release()
537
538               id_navbar_feedback_send_message.disabled =
539                 id_navbar_feedback_message1.value.length === 0
540               id_navbar_feedback_icon.hidden = false
541               //id_navbar_feedback_tick.hidden = true
542               id_navbar_feedback_cross.hidden = true
543               id_navbar_feedback_spinner.hidden = true
544               id_navbar_feedback_message.hidden = true
545             }
546
547             id_navbar_feedback_message1.addEventListener(
548               'input',
549               feedback_edited
550             )
551
552             id_navbar_feedback_send_message.addEventListener(
553               'click',
554               async () => {
555                 id_navbar_feedback_icon.hidden = false
556                 //id_navbar_feedback_tick.hidden = true
557                 id_navbar_feedback_cross.hidden = true
558                 id_navbar_feedback_spinner.hidden = true
559                 // the below causes an ugly flicker, so just keep the message
560                 //id_navbar_feedback_message.hidden = true
561
562                 if (!id_navbar_feedback_form.checkValidity()) {
563                   id_navbar_feedback_form.classList.add('was-validated');
564
565                   id_navbar_feedback_icon.hidden = true
566                   id_navbar_feedback_cross.hidden = false
567                   return
568                 }
569                 id_navbar_feedback_form.classList.remove('was-validated');
570
571                 id_navbar_feedback_icon.hidden = true
572                 id_navbar_feedback_spinner.hidden = false
573                 try {
574                   await api_call(
575                     '/api/feedback/send_message.json',
576                     location.href,
577                     id_navbar_feedback_message1.value.slice(0, 65536)
578                   )
579                 }
580                 catch (error) {
581                   let problem = Problem.from(error)
582
583                   id_navbar_feedback_cross.hidden = false
584                   id_navbar_feedback_spinner.hidden = true
585
586                   id_navbar_feedback_message.textContent = problem.detail
587                   //id_navbar_feedback_message.classList.remove('text-success')
588                   id_navbar_feedback_message.classList.add('text-danger')
589                   id_navbar_feedback_message.hidden = false
590                   return
591                 }
592                 //id_navbar_feedback_tick.hidden = false
593                 //id_navbar_feedback_spinner.hidden = true
594                 //id_navbar_feedback_message.textContent = 'We have received your message. We will be in touch as soon as possible.'
595                 //id_navbar_feedback_message.classList.add('text-success')
596                 //id_navbar_feedback_message.classList.remove('text-danger')
597                 //id_navbar_feedback_message.hidden = false
598
599                 id_navbar_feedback_icon.hidden = false
600                 id_navbar_feedback_spinner.hidden = true
601                 id_navbar_feedback_message.hidden = true
602                 id_navbar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
603                 $('#navbar-feedback-modal').modal('hide')
604                 $('#navbar-message-modal').modal('show')
605               }
606             )
607           }
608         )
609       }
610
611       await scripts(_out)
612     }
613   )
614 }