Update to new style navbar bar and _menu.json menu structure
authorNick Downing <nick@ndcode.org>
Fri, 7 Jan 2022 22:52:11 +0000 (09:52 +1100)
committerNick Downing <nick@ndcode.org>
Fri, 7 Jan 2022 22:54:51 +0000 (09:54 +1100)
14 files changed:
.gitignore
_config/globals.json
_menu.json [new file with mode: 0644]
_svg/icon_search_mono.svg [moved from _svg/icon_search.svg with 100% similarity]
_zet/index.sh
breadcrumbs.jst [new file with mode: 0644]
contact.html.jst [deleted file]
contact/index.html.jst [new file with mode: 0644]
contact_form.jst [deleted file]
feedback.html.jst
feedback_form.jst [deleted file]
index.html.jst
navbar.jst
search/index.html.jst [moved from search.html.jst with 60% similarity]

index dff39c0..d9822db 100644 (file)
@@ -1,5 +1,6 @@
 .*.deps
 .*.html
+.*.json
 .*.jst
 .*.less
 .*.min
index 671c41f..e7ef945 100644 (file)
@@ -1,15 +1,9 @@
 {
+  "site_url": "https://www.ndcode.org",
   "site_title": "NDCODE",
-  "navigation": [
-    "/index.html",
-    "/contact.html",
-    "/jsdoc/index.html",
-    "/sphinx/index.html"
-  ],
-  "page_to_title": {
-    "/index.html": "Home",
-    "/contact.html": "Contact",
-    "/jsdoc/index.html": "JSDoc",
-    "/sphinx/index.html": "Sphinx"
-  }
+  "contact_from": "NDCODE Contact <contact@ndcode.org>",
+  "contact_to": "Nick Downing <nick@ndcode.org>",
+  "feedback_from": "NDCODE Feedback <feedback@ndcode.org>",
+  "feedback_to": "Nick Downing <nick@ndcode.org>",
+  "copyright": "Integration Logic Pty Ltd trading as NDCODE and contributors"
 }
diff --git a/_menu.json b/_menu.json
new file mode 100644 (file)
index 0000000..1fa726e
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "entries": [
+    {"dir": "contact", "name": "Contact", "navbar": true},
+    {"dir": "jsdoc", "name": "JSDoc", "navbar": true},
+    {"dir": "sphinx", "name": "Sphinx", "navbar": true},
+    {"dir": "search", "name": "Search"}
+  ]
+}
index 88ec56c..fb70cef 100755 (executable)
@@ -2,7 +2,7 @@
 
 if test $# -lt 1
 then
-  url=http://localhost
+  url=http://localhost:8080
 else
   url="$1"
 fi
diff --git a/breadcrumbs.jst b/breadcrumbs.jst
new file mode 100644 (file)
index 0000000..424e9d1
--- /dev/null
@@ -0,0 +1,27 @@
+let assert = require('assert')
+
+return async (env, _out) => {
+  let pathname = env.parsed_url.pathname
+  assert(pathname.slice(0, 1) === '/')
+
+  // find number of path components, their positions, and names
+  let components = [{index: 0, name: 'Home'}]
+  for (let i = 1, j; (j = pathname.indexOf('/', i)) !== -1; i = j + 1) {
+    let menu = await env.site.get_menu(`${pathname.slice(0, i)}_menu.json`)
+    let dir = pathname.slice(i, j)
+    components.push({index: j, name: menu.entries[menu.index[dir]].name})
+  }
+
+  // present components as breadcrumbs, except last one as text
+  h2 {
+    for (let i = 0; i < components.length - 1; ++i) {
+      a.text-h4(
+        href=`${pathname.slice(0, components[i].index)}/index.html`
+      ) {`${components[i].name}`}
+      ' '
+      span.text-h5 {'>'}
+      ' '
+    }
+    `${components[components.length - 1].name}`
+  }
+}
diff --git a/contact.html.jst b/contact.html.jst
deleted file mode 100644 (file)
index 879760f..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-let querystring = require('querystring')
-let stream_buffers = require('stream-buffers')
-let XDate = require('xdate')
-
-return async env => {
-  let navbar = await _require('/navbar.jst')
-  let contact_form = await _require('/contact_form.jst')
-
-  await navbar(
-    env,
-    async _out => {},
-    async _out => {
-      h1 {'Contact NDCODE'}
-
-      div(style="height: 15px;") {}
-
-      if (env.request.method == 'POST') {
-        let write_stream = new stream_buffers.WritableStreamBuffer()
-        let data = new Promise(
-          (resolve, reject) => {
-            write_stream.
-            on('finish', () => {resolve(write_stream.getContents())}).
-            on('error', () => {reject()})
-          }
-        )
-        env.request.pipe(write_stream)
-        let query = querystring.parse((await data).toString())
-        let full_email = `${query.first_name} ${query.last_name} <${query.email}>`
-        console.log('received contact form:', full_email)
-
-        // save the form contents in a dated logfile, so that we can
-        // recover manually if the email doesn't send for some reason
-        date = new XDate()
-        query.date = date.toUTCString()
-
-        await env.site.ensure_dir('/_logs')
-        env.site.modify_json(
-          `/_logs/contact_${date.toUTCString('yyyyMMdd')}.json`,
-          [],
-          async result => {result.value.push(query)}
-        )
-
-        // send email (asynchronously)
-        ;(await env.site.get_emailjs('/_config/email_contact.json')).send(
-          {
-            text: query.message,
-            from: 'Contact form <contact@ndcode.org>',
-            'reply-to': full_email,
-            to: 'Nick Downing <nick@ndcode.org>',
-            subject:
-              Object.prototype.hasOwnProperty.call(query, 'company') ?
-              'Enquiry: ' + query.company :
-              'Enquiry'
-          },
-          (err, message) => {
-            if (err)
-              console.error(err.stack || err.message)
-            else
-              console.log('sent contact email:', full_email)
-          }
-        )
-
-        p {'Thanks! We\'ll be in touch as soon as we can.'}
-      }
-      else {
-        p {'Do you require more information or consulting assistance with integrating the projects on this site? We\'d love to hear from you.'}
-
-        await contact_form(env, _out)
-      }
-    }
-  )
-}
diff --git a/contact/index.html.jst b/contact/index.html.jst
new file mode 100644 (file)
index 0000000..f8bdb38
--- /dev/null
@@ -0,0 +1,135 @@
+let querystring = require('querystring')
+let stream_buffers = require('stream-buffers')
+let XDate = require('xdate')
+
+return async env => {
+  let breadcrumbs = await _require('/breadcrumbs.jst')
+  let globals = await env.site.get_json('/_config/globals.json')
+  let navbar = await _require('/navbar.jst')
+
+  await navbar(
+    env,
+    async _out => {},
+    async _out => {
+      await breadcrumbs(env, _out)
+
+      if (env.request.method == 'POST') {
+        let write_stream = new stream_buffers.WritableStreamBuffer()
+        let data = new Promise(
+          (resolve, reject) => {
+            write_stream.
+            on('finish', () => {resolve(write_stream.getContents())}).
+            on('error', () => {reject()})
+          }
+        )
+        env.request.pipe(write_stream)
+        let query = querystring.parse((await data).toString())
+        console.log('received contact form:', query.email)
+
+        // save the form contents in a dated logfile, so that we can
+        // recover manually if the email doesn't send for some reason
+        date = new XDate()
+        query.date = date.toUTCString()
+
+        await env.site.ensure_dir('/_logs')
+        env.site.modify_json(
+          `/_logs/contact_${date.toUTCString('yyyyMMdd')}.json`,
+          [],
+          async result => {result.value.push(query)}
+        )
+
+        // send email (asynchronously)
+        let emailjs_contact = await env.site.get_emailjs(
+          '/_config/email_contact.json'
+        )
+        emailjs_contact.send(
+          {
+            from: globals.contact_from,
+            'reply-to':
+              `${query.first_name} ${query.last_name} <${query.email}>`,
+            to: globals.contact_to,
+            subject:
+              Object.prototype.hasOwnProperty.call(query, 'company') ?
+              'Enquiry: ' + query.company :
+              'Enquiry',
+            text: query.message
+          },
+          (err, message) => {
+            if (err)
+              console.error(err.stack || err.message)
+            else
+              console.log('sent contact email:', query.email)
+          }
+        ) // ignore returned promise
+
+        p {'Thanks! We\'ll be in touch as soon as we can.'}
+      }
+      else {
+        p {'Do you require more information or consulting assistance with integrating the projects on this site? We\'d love to hear from you.'}
+
+        form#contact-form(method="post" action="index.html" role="form") {
+          div.row {
+            div.col-md-6 {
+              div.form-group {
+                label(for="contact_form_first_name") {'First name *'}
+                input.form-control#contact_form_first_name(type="text" name="first_name" placeholder="Please enter your first name" required="required" data-error="First name is required.") {}
+                div.help-block.with-errors {}
+              }
+            }
+            div.col-md-6 {
+              div.form-group {
+                label(for="contact_form_last_name") {'Last name *'}
+                input.form-control#contact_form_last_name(type="text" name="last_name" placeholder="Please enter your last name" required="required" data-error="Last name is required.") {}
+                div.help-block.with-errors {}
+              }
+            }
+          }
+          div.row {
+            div.col-md-6 {
+              div.form-group {
+                label(for="contact_form_company") {'Company'}
+                input.form-control#contact_form_company(type="text" name="company" placeholder="Please enter your company") {}
+                div.help-block.with-errors {}
+              }
+            }
+            div.col-md-6 {
+              div.form-group {
+                label(for="contact_form_email") {'Email *'}
+                input.form-control#contact_form_email(type="email" name="email" placeholder="Please enter your email" required="required" data-error="Valid email is required.") {}
+                div.help-block.with-errors {}
+              }
+            }
+          }
+          div.row {
+            div.col-md-12 {
+              div.form-group {
+                label(for="contact_form_message") {'Message *'}
+                textarea.form-control#contact_form_message(name="message" placeholder="Please explain your application" rows="4" required="required" data-error="Please, leave us a message.") {}
+                div.help-block.with-errors {}
+              }
+            }
+          }
+          p {} // fix this later
+          div.row {
+            div.col-md-12 {
+              input.btn.btn-success.btn-send(type="submit" value="Send message") {}
+            }
+          }
+          p {} // fix this later
+          div.row {
+            div.col-md-12 {
+              p.text-muted {
+                strong {'*'}
+                'These fields are required.'
+                //'Contact form template by '
+                //a(href="https://bootstrapious.com/p/how-to-build-a-working-bootstrap-contact-form" target="_blank") {'Bootstrapious'}
+                //'.'
+              }
+            }
+          }
+        }
+      }
+    },
+    async _out => {}
+  )
+}
diff --git a/contact_form.jst b/contact_form.jst
deleted file mode 100644 (file)
index 370a0ff..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-return async (env, _out) => {
-  form#contact-form(method="post" action="contact.html" role="form") {
-    div.row {
-      div.col-md-6 {
-        div.form-group {
-          label(for="contact_form_first_name") {'First name *'}
-          input.form-control#contact_form_first_name(type="text" name="first_name" placeholder="Please enter your first name" required="required" data-error="First name is required.") {}
-          div.help-block.with-errors {}
-        }
-      }
-      div.col-md-6 {
-        div.form-group {
-          label(for="contact_form_last_name") {'Last name *'}
-          input.form-control#contact_form_last_name(type="text" name="last_name" placeholder="Please enter your last name" required="required" data-error="Last name is required.") {}
-          div.help-block.with-errors {}
-        }
-      }
-    }
-    div.row {
-      div.col-md-6 {
-        div.form-group {
-          label(for="contact_form_company") {'Company'}
-          input.form-control#contact_form_company(type="company" name="company" placeholder="Please enter your company") {}
-          div.help-block.with-errors {}
-        }
-      }
-      div.col-md-6 {
-        div.form-group {
-          label(for="contact_form_email") {'Email *'}
-          input.form-control#contact_form_email(type="email" name="email" placeholder="Please enter your email" required="required" data-error="Valid email is required.") {}
-          div.help-block.with-errors {}
-        }
-      }
-    }
-    div.row {
-      div.col-md-12 {
-        div.form-group {
-          label(for="contact_form_message") {'Message *'}
-          textarea.form-control#contact_form_message(name="message" placeholder="Please explain your situation" rows="4" required="required" data-error="Please, leave us a message.") {}
-          div.help-block.with-errors {}
-        }
-      }
-    }
-    p {} // fix this later
-    div.row {
-      div.col-md-12 {
-        input.btn.btn-success.btn-send(type="submit" value="Send message") {}
-      }
-    }
-    p {} // fix this later
-    div.row {
-      div.col-md-12 {
-        p.text-muted {
-          strong {'*'}
-          'These fields are required.'
-          //'Contact form template by '
-          //a(href="https://bootstrapious.com/p/how-to-build-a-working-bootstrap-contact-form" target="_blank") {'Bootstrapious'}
-          //'.'
-        }
-      }
-    }
-  }
-}
index 6a3d1d9..11d951f 100644 (file)
@@ -3,7 +3,10 @@ let stream_buffers = require('stream-buffers')
 let XDate = require('xdate')
 
 return async env => {
-  if (env.request.method == 'POST') {
+  let globals = await env.site.get_json('/_config/globals.json')
+
+  let message
+  if (env.request.method === 'POST') {
     let write_stream = new stream_buffers.WritableStreamBuffer()
     let data = new Promise(
       (resolve, reject) => {
@@ -14,7 +17,7 @@ return async env => {
     )
     env.request.pipe(write_stream)
     let query = querystring.parse((await data).toString())
-    console.log('received feedback form:', JSON.stringify(query)) //query.page)
+    console.log('received feedback form:', query.page)
 
     // save the form contents in a dated logfile, so that we can
     // recover manually if the email doesn't send for some reason
@@ -29,12 +32,15 @@ return async env => {
     )
 
     // send email (asynchronously)
-    ;(await env.site.get_emailjs('/_config/email_feedback.json')).send(
+    let emailjs_feedback = await env.site.get_emailjs(
+      '/_config/email_feedback.json'
+    )
+    emailjs_feedback.send(
       {
+        from: globals.feedback_from,
+        to: globals.feedback_to,
+        subject: 'Page: ' + query.page,
         text: query.message,
-        from: 'Feedback form <feedback@ndcode.org>',
-        to: 'Nick Downing <nick@ndcode.org>',
-        subject: 'Page: ' + query.page
       },
       (err, message) => {
         if (err)
@@ -43,6 +49,11 @@ return async env => {
           console.log('sent feedback email:', query.page)
       }
     )
+
+    message = 'Thanks!'
   }
-  env.site.serve(env, 200, Buffer.from('Thanks!'), 'feedback_form.html.jst')
+  else
+    message = 'Please POST.'
+
+  env.site.serve(env, 200, Buffer.from(message), 'feedback.html.jst')
 }
diff --git a/feedback_form.jst b/feedback_form.jst
deleted file mode 100644 (file)
index 402e868..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-return async (env, _out) => {
-  form#feedback-form(method="post" action="feedback.html" role="form") {
-    div.row {
-      div.col-md-12 {
-        div.form-group {
-          label(for="feedback-form-message") {'Message *'}
-          textarea.form-control#feedback-form-message(name="message" placeholder="Please tell us your thoughts" rows="4" required="required" data-error="Please, leave us a message.") {}
-          div.help-block.with-errors {}
-        }
-      }
-    }
-    p {} // fix this later
-    div.row {
-      div.col-md-12 {
-        p.text-muted {
-          strong {'*'}
-          'These fields are required.'
-          //'Contact form template by '
-          //a(href="https://bootstrapious.com/p/how-to-build-a-working-bootstrap-feedback-form" target="_blank") {'Bootstrapious'}
-          //'.'
-        }
-      }
-    }
-    input.btn.btn-success.btn-send(style="display: none;" type="submit" value="Send message") {}
-  }
-}
index 5375d31..a19c10b 100644 (file)
@@ -1,19 +1,25 @@
 return async env => {
+  let breadcrumbs = await _require('/breadcrumbs.jst')
   let icon_jst = await env.site.get_min_svg('/_svg/icon_jst.svg')
   let navbar = await _require('/navbar.jst')
   let icon_pitree = await env.site.get_min_svg('/_svg/icon_pitree.svg')
  
   await navbar(
     env,
+    // head
     async _out => {},
+    // body
     async _out => {
-      h1 {'NDCODE projects'}
+      await breadcrumbs(env, _out)
 
       p {'We have developed these projects because we wanted to do things differently â€” and make our vision come to life. At times our vision is quite radical and creative, at other times we just fall back on good engineering and making it as simple as possible.'}
 
       p {'Given that we do things differently, we have generally had to build up each idea into a complete framework that provides a comprehensive solution to a family of problems. Please do contribute to help make each framework more comprehensive over time.'}
 
-      p {'Go on, take a look inside!'}
+      p {
+        'Go on, take a look inside! '
+        i {'[Actually don\'t because it\'s still under construction]'}
+      }
 
       ul.nav.nav-stacked {
         li {
@@ -52,6 +58,8 @@ return async env => {
           }
         }
       }
-    }
+    },
+    // scripts
+    async _out => {}
   )
 }
index 305961c..076b59d 100644 (file)
@@ -1,21 +1,38 @@
+let assert = require('assert')
 let XDate = require('xdate')
 
-return async (env, head, body) => {
-  let feedback_form = await _require('/feedback_form.jst')
+return async (env, head, body, scripts) => {
+  //let cart = await _require('/online_store/cart.jst')
   let globals = await env.site.get_json('/_config/globals.json')
-  let icon_search = await env.site.get_min_svg('/_svg/icon_search.svg')
+  //let icon_cart_small = await env.site.get_min_svg('/_svg/icon_cart_small.svg')
+  let icon_search_mono = await env.site.get_min_svg('/_svg/icon_search_mono.svg')
   let logo_large = await env.site.get_min_svg('/_svg/logo_large.svg')
+  let menu = await env.site.get_menu('/_menu.json')
   let page = await _require('/page.jst')
+  //let session = await _require('/session.jst')
+
+  // initialize env.sessions, env.session_key, env.session
+  //await session(env)
+
+  // initialize env.cart
+  //await cart(env)
 
   await page(
     env,
     // head
     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)
+
       title {
         _out.push(
-          globals.site_title +
-          ": " +
-          (globals.page_to_title[env.parsed_url.pathname] || env.parsed_url.pathname)
+          globals.site_title + ': ' + (
+            dir.length === 0 ?
+            'Home' :
+            menu.entries[menu.index[dir]].name
+          )
         )
       }
 
@@ -23,36 +40,73 @@ return async (env, head, body) => {
     },
     // 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(style="padding-left: calc(100vw - 100%);") {
-        div.container(style="margin-top: 15px; margin-bottom: 15px;") {
-          //div.row {
-          //  div.col-sm-12(style="text-align: right;") {
-          //    a(href="/login.html") {'Login'}
-          //  }
-          //}
+        div.container(style="margin-top: 5px; margin-bottom: 5px;") {
           div.row {
             div.'col-sm-8'.vbottom {
               _out.push(logo_large)
             }
             div.'col-sm-4'.vbottom(style="padding-bottom: 15px;") {
-              form(action="/search.html") {
+              //div {
+              //  let signed_in =
+              //    Object.prototype.hasOwnProperty.call(env.session, 'account')
+              //  span#signed-in-status {
+              //    if (signed_in)
+              //      `Signed in as ${env.session.account}.`
+              //    else
+              //      'Browsing as guest.'
+              //  }
+              //  ' '
+              //  if (signed_in)
+              //    a#sign-in(href="" hidden="") {'Sign in'}
+              //  else
+              //    a#sign-in(href="") {'Sign in'}
+              //  ' '
+              //  if (signed_in)
+              //    a#sign-out(href="") {'Sign out'}
+              //  else
+              //    a#sign-out(href="" hidden="") {'Sign out'}
+              //}
+              //p {}
+
+              form(action="/search/index.html") {
                 div.input-group {
                   input.form-control(name="query" type="text" placeholder="Search") {}
                   span.input-group-btn {
                     button.btn.btn-default(type="submit") {
-                      _out.push(icon_search)
+                      _out.push(icon_search_mono)
                     }
                   }
                 }
               }
             }
+            //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-default(style="margin-top: 0px; margin-bottom: 0px;") {
         div(style="padding-left: calc(100vw - 100%);") {
           div.container { //-fluid") {
-            //  Brand and toggle get grouped for better mobile display 
+            //  Brand and toggle get grouped for better mobile display
             div.navbar-header {
               button.navbar-toggle.collapsed(type="button" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false") {
                 span.sr-only {'Toggle navigation'}
@@ -63,36 +117,49 @@ return async (env, head, body) => {
               //a.navbar-brand(href="#") {'Brand'}
             }
 
-            //  Collect the nav links, forms, and other content for toggling 
+            //  Collect the nav links, forms, and other content for toggling
             div.collapse.navbar-collapse#bs-example-navbar-collapse-1 {
               ul.nav.navbar-nav {
-                let navigation = globals.navigation
-                for (let i = 0; i < navigation.length; ++i) {
-                  let page = navigation[i]
-                  let title = globals.page_to_title[page] || page
-                  if (page === env.parsed_url.pathname)
-                    li.active {
-                      a(href=page) {
-                        `${title}`
-                        span.sr-only {'(current)'}
-                      }
+                if (dir.length === 0)
+                  li.active {
+                    a(href="/index.html") {
+                      'Home'
+                      span.sr-only {'(current)'}
                     }
-                  else
-                    li {
-                      a(href=page) {`${title}`}
+                  }
+                else
+                  li {
+                    a(href="/index.html") {
+                      'Home'
                     }
-                }
+                  }
+                let entries = menu.entries
+                for (let i = 0; i < entries.length; ++i)
+                  if (entries[i].navbar)
+                    if (entries[i].dir === dir)
+                      li.active {
+                        a(href=`/${entries[i].dir}/index.html`) {
+                          `${entries[i].name}`
+                          span.sr-only {'(current)'}
+                        }
+                      }
+                    else
+                      li {
+                        a(href=`/${entries[i].dir}/index.html`) {
+                          `${entries[i].name}`
+                        }
+                      }
               }
               ul.nav.navbar-nav.navbar-right {
                 li {
-                  a#give-feedback {'Give feedback'}
+                  a#give-feedback(href="") {'Give feedback'}
                 }
-              } 
+              }
             }
           }
         }
       }
-      div(style="padding-left: calc(100vw - 100%);") {
+      div(style="padding-left: calc(100vw - 100%); margin-bottom: 50px;") {
         div.container {
           await body(_out)
         }
@@ -116,11 +183,67 @@ return async (env, head, body) => {
 
           p {'Example code fragments embedded within the text are placed in the public domain unless otherwise noted.'}
 
-          p {`Copyright © ${new XDate().getUTCFullYear()} Integration Logic Pty Ltd trading as NDCODE and contributors.`}
+          p {`Copyright © ${new XDate().getUTCFullYear()} ${globals.copyright}.`}
         }
       }
 
       // hidden part
+      //div#sign-in-modal.modal.fade(role="dialog") {
+      //  div.modal-dialog {
+      //    div.modal-content {
+      //      div.modal-header {
+      //        button.close(type="button" data-dismiss="modal") {
+      //          '×'
+      //        }
+      //        h4.modal-title {
+      //          'Sign in'
+      //        }
+      //      }
+      //      div.modal-body {
+      //        form#sign-in-form(method="post" action="/sign_in.json" role="form") {
+      //          div.row {
+      //            div.col-md-12 {
+      //              div.form-group {
+      //                label(for="sign-in-form-email") {'Email'}
+      //                input.form-control#sign-in-form-email(type="text" name="email" placeholder="Please enter your email address" required="required" data-error="Email address is required.") {}
+      //                div.help-block.with-errors {}
+      //              }
+      //            }
+      //          }
+      //          div.row {
+      //            div.col-md-12 {
+      //              div.form-group {
+      //                label(for="sign-in-form-password") {'Password'}
+      //                input.form-control#sign-in-form-password(type="password" name="password" required="required" placeholder="Please enter your password" data-error="Password is required.") {}
+      //                div.help-block.with-errors {}
+      //              }
+      //            }
+      //          }
+      //          input.btn.btn-success.btn-send(style="display: none;" type="submit" value="Sign in") {}
+      //        }
+
+      //        p {
+      //          'No account yet? '
+      //          a(href="/my_account/sign_up/index.html") {'Sign up'}
+      //        }
+
+      //        p {
+      //          'Forgot password? '
+      //          a(href="/my_account/password_reset/index.html") {'Password reset'}
+      //        }
+      //      }
+      //      div.modal-footer {
+      //        button.btn.btn-primary(type="submit" form="sign-in-form") {
+      //          'Sign in'
+      //        }
+      //        button.btn.btn-default(type="button" data-dismiss="modal") {
+      //          'Cancel'
+      //        }
+      //      }
+      //    }
+      //  }
+      //}
+
       div#feedback-modal.modal.fade(role="dialog") {
         div.modal-dialog {
           div.modal-content {
@@ -136,14 +259,37 @@ return async (env, head, body) => {
               p {
                 'Did you notice something not quite right, or just want to share your impression of this page?'
               }
-              await feedback_form(env, _out)
+              form#feedback-form(method="post" action="/feedback.html" role="form") {
+                div.row {
+                  div.col-md-12 {
+                    div.form-group {
+                      label(for="feedback-form-message") {'Message *'}
+                      textarea.form-control#feedback-form-message(name="message" placeholder="Please tell us your thoughts" rows="4" required="required" data-error="Please, leave us a message.") {}
+                      div.help-block.with-errors {}
+                    }
+                  }
+                }
+                p {} // fix this later
+                div.row {
+                  div.col-md-12 {
+                    p.text-muted {
+                      strong {'*'}
+                      'These fields are required.'
+                      //'Contact form template by '
+                      //a(href="https://bootstrapious.com/p/how-to-build-a-working-bootstrap-feedback-form" target="_blank") {'Bootstrapious'}
+                      //'.'
+                    }
+                  }
+                }
+                input.btn.btn-success.btn-send(style="display: none;" type="submit" value="Send message") {}
+              }
             }
             div.modal-footer {
               button.btn.btn-primary(type="submit" form="feedback-form") {
                 'Submit'
               }
               button.btn.btn-default(type="button" data-dismiss="modal") {
-                'Close'
+                'Cancel'
               }
             }
           }
@@ -175,39 +321,157 @@ return async (env, head, body) => {
     },
     // scripts
     async _out => {
-      // when feedback form is submitted, do not reload the page
+      //script(src="/js/sha256.js") {}
+
       script {
-        $(document).ready(function() {
-          $('#give-feedback').click(function() {
-            $('#feedback-modal').modal('show')
-            $('#feedback-form-message').text('')
-          })
-          $('#feedback-modal').on('shown.bs.modal', function() {
-            $('#feedback-form-message').focus()
-          })
-          $(document).on('submit', '#feedback-form', function(e) {
-            e.preventDefault()
-            $.ajax({
-              url: '/feedback.html',
-              type: 'POST',
-              data: {
-                page: window.location.href,
-                message: $('#feedback-form-message').val()
-              },
-              success: function(data, textStatus, jqXHR) {
-                $('#feedback-modal').modal('hide')
-                $('#message-modal-message').text(data)
-                $('#message-modal').modal('show')
-              },
-              error: function(jqXHR, textStatus, errorThrown) {
-                $('#feedback-modal').modal('hide')
-                $('#message-modal-message').text(errorThrown)
-                $('#message-modal').modal('show')
-              }               
-            })
-          })
-        })
+        //function get_cookie(name) {
+        //  let entries = document.cookie.split(';');
+        //  for (let i = 0; i < entries.length; ++i) {
+        //    let fields = entries[i].split('=');
+        //    if (fields[0].trim() === name)
+        //      return decodeURIComponent(fields[1]);
+        //  }
+        //  return undefined;
+        //}
+
+        //// this function can be overridden in a further script
+        //function sign_in_out(status) {
+        //}
+
+        $(document).ready(
+          () => {
+            //// sign in form
+            //$('#sign-in').click(
+            //  () => {
+            //    $('#sign-in-form-email').text('')
+            //    $('#sign-in-form-password').text('')
+            //    $('#sign-in-modal').modal('show')
+            //    return false
+            //  }
+            //)
+            //$('#sign-in-modal').on(
+            //  'shown.bs.modal',
+            //  () => {
+            //    $('#sign-in-form-email').focus()
+            //  }
+            //)
+            //// when sign in form is submitted, do not reload the page
+            //$(document).on(
+            //  'submit',
+            //  '#sign-in-form',
+            //  e => {
+            //    e.preventDefault()
+            //    $.ajax(
+            //      {
+            //        url: '/my_account/sign_in.json',
+            //        type: 'POST',
+            //        data: {
+            //          email: $('#sign-in-form-email').val(),
+            //          password: sha256(
+            //            get_cookie('session_key') +
+            //            $('#sign-in-form-password').val()
+            //          ).toString('hex')
+            //        },
+            //        success: (data, textStatus, jqXHR) => {
+            //          $('#sign-in-modal').modal('hide')
+            //          switch (data.result) {
+            //          case 1: // success
+            //            $('#signed-in-status').text(data.signed_in_status)
+            //            $('#sign-in').hide()
+            //            $('#sign-out').show()
+            //            sign_in_out(true) // notify navbar caller
+            //            break
+            //          case 2: // redirect
+            //            location.href = data.redirect_href
+            //            break
+            //          }
+            //          $('#message-modal-message').text(data.message)
+            //          $('#message-modal').modal('show')
+            //        },
+            //        error: (jqXHR, textStatus, errorThrown) => {
+            //          $('#sign-in-modal').modal('hide')
+            //          $('#message-modal-message').text(errorThrown)
+            //          $('#message-modal').modal('show')
+            //        }
+            //      }
+            //    )
+            //  }
+            //)
+
+            //// sign out button
+            //$('#sign-out').click(
+            //  () => {
+            //    $.ajax(
+            //      {
+            //        url: '/my_account/sign_out.json',
+            //        type: 'GET',
+            //        success: (data, textStatus, jqXHR) => {
+            //          if (data.result) {
+            //            $('#signed-in-status').text(data.signed_in_status)
+            //            $('#sign-in').show()
+            //            $('#sign-out').hide()
+            //            sign_in_out(false) // notify navbar caller
+            //          }
+            //          $('#message-modal-message').text(data.message)
+            //          $('#message-modal').modal('show')
+            //        },
+            //        error: (jqXHR, textStatus, errorThrown) => {
+            //          $('#message-modal-message').text(errorThrown)
+            //          $('#message-modal').modal('show')
+            //        }
+            //      }
+            //    )
+            //    return false
+            //  }
+            //)
+
+            // feedback form
+            $('#give-feedback').click(
+              () => {
+                $('#feedback-form-message').text('')
+                $('#feedback-modal').modal('show')
+                return false
+              }
+            )
+            $('#feedback-modal').on(
+              'shown.bs.modal',
+              () => {
+                $('#feedback-form-message').focus()
+              }
+            )
+            // when feedback form is submitted, do not reload the page
+            $(document).on(
+              'submit',
+              '#feedback-form',
+              e => {
+                e.preventDefault()
+                $.ajax(
+                  {
+                    url: '/feedback.html',
+                    type: 'POST',
+                    data: {
+                      page: window.location.href,
+                      message: $('#feedback-form-message').val()
+                    },
+                    success: (data, textStatus, jqXHR) => {
+                      $('#feedback-modal').modal('hide')
+                      $('#message-modal-message').text(data)
+                      $('#message-modal').modal('show')
+                    },
+                    error: (jqXHR, textStatus, errorThrown) => {
+                      $('#feedback-modal').modal('hide')
+                      $('#message-modal-message').text(errorThrown)
+                      $('#message-modal').modal('show')
+                    }
+                  }
+                )
+              }
+            )
+          }
+        )
       }
+
+      await scripts(_out)
     }
   )
 }
similarity index 60%
rename from search.html.jst
rename to search/index.html.jst
index cad9570..e80bbfe 100644 (file)
@@ -1,7 +1,8 @@
+let assert = require('assert')
 let querystring = require('querystring')
 
 return async env => {
-  let globals = await env.site.get_json('/_config/globals.json')
+  let breadcrumbs = await _require('/breadcrumbs.jst')
   let navbar = await _require('/navbar.jst')
   let zet_site = await env.site.get_zettair('/_zet/site')
 
@@ -16,7 +17,30 @@ return async env => {
     env,
     async _out => {},
     async _out => {
-      h1 {'Search results'}
+      async function breadcrumbs_str(pathname) {
+        assert(pathname.slice(0, 1) === '/')
+      
+        // find names of path components
+        console.log('pathname', pathname)
+        let components = ['Home']
+        for (let i = 1, j; (j = pathname.indexOf('/', i)) !== -1; i = j + 1) {
+          let menu
+          try {
+            menu = await env.site.get_menu(`${pathname.slice(0, i)}_menu.json`)
+          }
+          catch (e) {
+            return pathname // fallback
+          }
+          let dir = pathname.slice(i, j)
+          if (!Object.prototype.hasOwnProperty.call(menu.index, dir))
+            return pathname // fallback
+          components.push(menu.entries[menu.index[dir]].name)
+        }
+
+        return components.join(' > ')      
+      }
+
+      await breadcrumbs(env, _out)
 
       h4 {
         'Query: '
@@ -30,7 +54,7 @@ return async env => {
           for (let i = 0; i < search.results.length; ++i) {
             let page = search.results[i].auxiliary
             li {
-              a(href=page) {`${globals.page_to_title[page] || page}`}
+              a(href=page) {`${await breadcrumbs_str(page)}`}
               br {}
               p {_out.push(search.results[i].summary)} // note: contains HTML
             }
@@ -39,7 +63,7 @@ return async env => {
 
         ul.pagination {
           for (let i = 0; i * 10 < search.total_results; ++i) {
-            let page = '/search.html?' + querystring.stringify(
+            let page = '/search/index.html?' + querystring.stringify(
               {query: query, first: i * 10}
             )
             let text = (i + 1).toString()
@@ -59,6 +83,7 @@ return async env => {
       }
       else
         p {'No results'}
-    }
+    },
+    async _out => {}
   )
 }