diff --git a/app.js b/app.js index 8e1e68a..842f21c 100644 --- a/app.js +++ b/app.js @@ -14,7 +14,7 @@ const categories = require('./routes/categories') const about = require('./routes/about') const settings = require('./routes/settings') const sitemap = require('./routes/sitemap') -const {initClient} = require('./services/contentful') +const { initClient, getSpace } = require('./services/contentful') const breadcrumb = require('./lib/breadcrumb') const app = express() @@ -31,21 +31,58 @@ app.use(cookieParser()) app.use(express.static(path.join(__dirname, 'public'))) app.use(breadcrumb()) -// Pass custom helpers to all our templates -app.use(function (req, res, next) { +// Pass our application state and custom helpers to all our templates +app.use(async function (req, res, next) { + // Inject ucstom helpers res.locals.helpers = helpers + + // Express query string const qs = url.parse(req.url).query res.locals.queryString = qs ? `?${qs}` : '' res.locals.query = req.query res.locals.currentPath = req.path - // Allow setting of credentials via query parameters + + // Allow setting of API credentials via query parameters + let settings = req.cookies.theExampleAppSettings + const { space_id, preview_access_token, delivery_access_token } = req.query if (space_id && preview_access_token && delivery_access_token) { // eslint-disable-line camelcase - const settings = {space: space_id, cda: delivery_access_token, cpa: preview_access_token} + settings = {space: space_id, cda: delivery_access_token, cpa: preview_access_token} res.cookie('theExampleAppSettings', settings, { maxAge: 900000, httpOnly: true }) - initClient(settings) - } else { - initClient(req.cookies.theExampleAppSettings) + } + + initClient(settings) + res.locals.settings = settings + + // Manage language and API type state and make it globally available + const apis = [ + { + id: 'cda', + label: 'Delivery (published content)' + }, + { + id: 'cpa', + label: 'Preview (draft content)' + } + ] + + res.locals.currentApi = apis + .find((api) => api.id === (req.query.api || 'cda')) + + // Get enabled locales from Contentful + const space = await getSpace() + res.locals.locales = space.locales + + const defaultLocale = res.locals.locales + .find((locale) => locale.default) + + if (req.query.locale) { + res.locals.currentLocale = space.locales + .find((locale) => locale.code === req.query.locale) + } + + if (!res.locals.currentLocale) { + res.locals.currentLocale = defaultLocale } next() diff --git a/assets/stylesheets/global/header.css b/assets/stylesheets/global/header.css index 83ff5f0..46d9028 100644 --- a/assets/stylesheets/global/header.css +++ b/assets/stylesheets/global/header.css @@ -27,22 +27,11 @@ justify-content: space-between; align-items: center; - margin-bottom: calc(var(--grid-gutter) / 2); - @media (--breakpoint-desktop) { flex: 0 1 auto; flex-direction: row; margin-bottom: 0; } - - & div + div { - margin: calc(var(--grid-gutter) / 2) 0 0; - - - @media (--breakpoint-desktop) { - margin: 0 0 0 calc(var(--grid-gutter) / 2); - } - } } @element upper-second { @@ -56,14 +45,6 @@ flex: 0 1 auto; flex-direction: row; } - - & div + div { - margin: calc(var(--grid-gutter) / 2) 0 0; - - @media (--breakpoint-desktop) { - margin: 0 0 0 calc(var(--grid-gutter) / 2); - } - } } @element upper-link { @@ -73,6 +54,11 @@ @element upper-copy { display: inline-flex; + margin: 0 0 calc(var(--grid-gutter) / 2); + + @media (--breakpoint-desktop) { + margin: 0 calc(var(--grid-gutter) / 2) 0 0; + } } @element upper-icon { @@ -107,6 +93,10 @@ height: auto; margin-right: calc(var(--grid-gutter) / 2); } + + @media (--breakpoint-desktop) { + margin: 0 calc(var(--grid-gutter) / 2) 0 0; + } } @element logo { @@ -134,52 +124,98 @@ @media (--breakpoint-desktop) { flex: 0 0 auto; + flex-direction: row; + } + } + + @element controls_group { + flex: 0 0 auto; + position: relative; + + display: flex; + margin: 0 0 calc(var(--grid-gutter) / 2); + padding: 0 calc(var(--grid-gutter) / 2); + + border: none; + background: var(--color-palette-blue-medium); + border-radius: var(--border-radius); + color: inherit; + + &:last-child { + margin: 0; } - & form { - display: flex; - flex-wrap: wrap; - justify-content: center; + @media (--breakpoint-desktop) { + margin: 0 calc(var(--grid-gutter) / 2) 0 0; + } + } - @media (--breakpoint-desktop) { - justify-content: flex-end; - } + @element controls_label { + position: relative; + z-index: 1; + font-family: var(--font-medium); + cursor: pointer; + } - & .group { - flex: 0 0 auto; - display: flex; - padding: 0 calc(var(--grid-gutter) / 2); - margin: 0; + @element controls_dropdown { + position: absolute; + z-index: 2; + box-sizing: border-box; - border: none; - background: var(--color-palette-blue-medium); - border-radius: var(--border-radius); - color: inherit; + width: 260px; + max-width: 90vw; + margin: calc(var(--grid-gutter) / 2 - 4px) 0 0; - & + .group { - margin-left: var(--grid-gutter) - } + opacity: 0; + background: #fff; + border-radius: 2px; + box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.1); - & select, - & label { - flex: 0 0 auto; - display: inline-block; - margin-top: 0; - margin-bottom: 0; + transition: 0.3s opacity ease; + pointer-events: none; - color: inherit; - font-weight: normal; - font-family: var(--font-medium); - font-size: 0.8em; + @modifier active { + opacity: 1; + pointer-events: all; - background: transparent; - border: none; - } + &:before { + content: ''; + position: absolute; + left: 50%; + top: -5px; + width: 10px; + height: 10px; - & select { - margin-top: 2px; - } + background-color: inherit; + transform: translateX(-50%) rotate(45deg); } } } + + @element controls_help_text { + padding: calc(var(--grid-gutter) / 2); + color: var(--color-text-grey); + } + + @element controls_button { + box-sizing: border-box; + height: auto; + width: 100%; + margin: 0; + + border-radius: 0; + background: var(--header-dropdown-bg); + + color: var(--header-dropdown-btn-color); + font-size: 1em; + + &:hover { + background: var(--header-dropdown-bg-hover); + color: var(--header-dropdown-btn-color); + } + + @modifier active { + background: var(--header-dropdown-bg-active); + } + } } diff --git a/assets/stylesheets/layout/layout-sidebar.css b/assets/stylesheets/layout/layout-sidebar.css index b1604c8..2b15ee1 100644 --- a/assets/stylesheets/layout/layout-sidebar.css +++ b/assets/stylesheets/layout/layout-sidebar.css @@ -40,8 +40,9 @@ padding-top: calc(var(--grid-gutter) / 4); @media (--breakpoint-desktop) { - flex: 0 1 auto; - width: var(--layout-sidebar-content-width); + flex: 0 1 var(--layout-sidebar-content-width); + width: 100%; + box-sizing: border-box; margin-left: var(--grid-gutter); } diff --git a/assets/stylesheets/variables.css b/assets/stylesheets/variables.css index 04d7767..bae8656 100644 --- a/assets/stylesheets/variables.css +++ b/assets/stylesheets/variables.css @@ -58,6 +58,11 @@ --cta-bg-hover: var(--color-palette-blue-medium); --cta-radius: 3px; + --header-dropdown-btn-color: #263545; + --header-dropdown-bg: #fff; + --header-dropdown-bg-active: #d3dce0; + --header-dropdown-bg-hover: #e5ebed; + --module-copy-emphasized-bg: #8091a5; --module-copy-emphasized-color: #c3cfd5; --module-copy-emphasized-headlines-color: #fff; diff --git a/helpers.js b/helpers.js index 690db2c..3ee18aa 100644 --- a/helpers.js +++ b/helpers.js @@ -15,4 +15,3 @@ function removeIvalidDataURL (content) { let regex = /data:\S+;base64\S*/gm return content.replace(regex, '#') } - diff --git a/public/scripts/hoverintent.min.js b/public/scripts/hoverintent.min.js new file mode 100755 index 0000000..4b58e5b --- /dev/null +++ b/public/scripts/hoverintent.min.js @@ -0,0 +1 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.hoverintent=e()}}(function(){return function e(t,n,o){function r(u,f){if(!n[u]){if(!t[u]){var s="function"==typeof require&&require;if(!f&&s)return s(u,!0);if(i)return i(u,!0);var c=new Error("Cannot find module '"+u+"'");throw c.code="MODULE_NOT_FOUND",c}var a=n[u]={exports:{}};t[u][0].call(a.exports,function(e){var n=t[u][1][e];return r(n||e)},a,a.exports,e,t,n,o)}return n[u].exports}for(var i="function"==typeof require&&require,u=0;u { + // Header dropdowns + const controls = document.querySelectorAll('.header__controls_group') + + controls.forEach((control) => { + const ref = control.querySelector('.header__controls_label') + const dropdownRef = control.querySelector('.header__controls_dropdown') + + let popper = null + + hoverintent(control, null, () => { + if (popper) { + dropdownRef.classList.remove('header__controls_dropdown--active') + window.setTimeout(popper.destroy, 500) + } + }).options({ + sensitivity: 10, + interval: 150, + timeout: 300 + }) + + ref.addEventListener('click', (e) => { + dropdownRef.classList.add('header__controls_dropdown--active') + popper = new Popper( + e.target, + dropdownRef, + { + // popper options here + } + ) + }) + }) + // const apiRef = + // const apiDropdownRef = document.querySelector('#control-api .header__controls_dropdown') + + // const apiTooltip = + // Init highlight.js hljs.initHighlightingOnLoad() diff --git a/public/scripts/popper.min.js b/public/scripts/popper.min.js new file mode 100644 index 0000000..4553f65 --- /dev/null +++ b/public/scripts/popper.min.js @@ -0,0 +1,5 @@ +/* + Copyright (C) Federico Zivolo 2017 + Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). + */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=window.getComputedStyle(e,null);return t?o[t]:o}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e||-1!==['HTML','BODY','#document'].indexOf(e.nodeName))return window.document.body;var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll)/.test(r+s+p)?e:n(o(e))}function r(e){var o=e&&e.offsetParent,i=o&&o.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(o.nodeName)&&'static'===t(o,'position')?r(o):o:window.document.documentElement}function p(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||r(e.firstElementChild)===e)}function s(e){return null===e.parentNode?e:s(e.parentNode)}function d(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return window.document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,i=o?e:t,n=o?t:e,a=document.createRange();a.setStart(i,0),a.setEnd(n,0);var l=a.commonAncestorContainer;if(e!==l&&t!==l||i.contains(n))return p(l)?l:r(l);var f=s(e);return f.host?d(f.host,t):d(e,s(t).host)}function a(e){var t=1=o.clientWidth&&i>=o.clientHeight}),l=0i[e]&&!t.escapeWithReference&&(n=V(p[o],i[e]-('right'===e?p.width:p.height))),se({},o,n)}};return n.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';p=de({},p,s[t](e))}),e.offsets.popper=p,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,i=t.reference,n=e.placement.split('-')[0],r=_,p=-1!==['top','bottom'].indexOf(n),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(i[s])&&(e.offsets.popper[d]=r(i[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){if(!F(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var n=e.placement.split('-')[0],r=e.offsets,p=r.popper,s=r.reference,d=-1!==['left','right'].indexOf(n),a=d?'height':'width',l=d?'Top':'Left',f=l.toLowerCase(),m=d?'left':'top',c=d?'bottom':'right',g=O(i)[a];s[c]-gp[c]&&(e.offsets.popper[f]+=s[f]+g-p[c]);var u=s[f]+s[a]/2-g/2,b=t(e.instance.popper,'margin'+l).replace('px',''),y=u-h(e.offsets.popper)[f]-b;return y=X(V(p[a]-g,y),0),e.arrowElement=i,e.offsets.arrow={},e.offsets.arrow[f]=Math.round(y),e.offsets.arrow[m]='',e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=w(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement),i=e.placement.split('-')[0],n=L(i),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case fe.FLIP:p=[i,n];break;case fe.CLOCKWISE:p=K(i);break;case fe.COUNTERCLOCKWISE:p=K(i,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(i!==s||p.length===d+1)return e;i=e.placement.split('-')[0],n=L(i);var a=e.offsets.popper,l=e.offsets.reference,f=_,m='left'===i&&f(a.right)>f(l.left)||'right'===i&&f(a.left)f(l.top)||'bottom'===i&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===i&&c||'right'===i&&h||'top'===i&&g||'bottom'===i&&u,y=-1!==['top','bottom'].indexOf(i),w=!!t.flipVariations&&(y&&'start'===r&&c||y&&'end'===r&&h||!y&&'start'===r&&g||!y&&'end'===r&&u);(m||b||w)&&(e.flipped=!0,(m||b)&&(i=p[d+1]),w&&(r=j(r)),e.placement=i+(r?'-'+r:''),e.offsets.popper=de({},e.offsets.popper,S(e.instance.popper,e.offsets.reference,e.placement)),e=N(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],i=e.offsets,n=i.popper,r=i.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return n[p?'left':'top']=r[o]-(s?n[p?'width':'height']:0),e.placement=L(t),e.offsets.popper=h(n),e}},hide:{order:800,enabled:!0,fn:function(e){if(!F(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=T(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.right category.fields.slug === req.params.category) - courses = await getCoursesByCategory(activeCategory.sys.id, req.query.locale, req.query.api) + courses = await getCoursesByCategory(activeCategory.sys.id, res.locals.currentLocale.code, res.locals.currentLocale.id) } catch (e) { console.log('Error ', e) } @@ -31,7 +31,7 @@ router.get('/categories/:category', catchErrors(async function (req, res, next) /* GET course detail. */ const courseRoute = catchErrors(async function (req, res, next) { - let course = await getCourse(req.params.slug, req.query.locale, req.query.api) + let course = await getCourse(req.params.slug, res.locals.currentLocale.code, res.locals.currentLocale.id) const lessons = course.fields.lessons const lessonIndex = lessons.findIndex((lesson) => lesson.fields.slug === req.params.lslug) const lesson = lessons[lessonIndex] @@ -47,13 +47,13 @@ router.get('/:slug/lessons', courseRoute) /* GET course lesson detail. */ router.get('/:cslug/lessons/:lslug', catchErrors(async function (req, res, next) { - let course = await getCourse(req.params.cslug, req.query.locale, req.query.api) + let course = await getCourse(req.params.cslug, res.locals.currentLocale.code, res.locals.currentLocale.id) const lessons = course.fields.lessons const lessonIndex = lessons.findIndex((lesson) => lesson.fields.slug === req.params.lslug) const lesson = lessons[lessonIndex] const nextLesson = lessons[lessonIndex + 1] || null const cookie = req.cookies.visitedLessons - let visitedLessons = cookie || [] + let visitedLessons = cookie || [] visitedLessons.push(lesson.sys.id) visitedLessons = [...new Set(visitedLessons)] res.cookie('visitedLessons', visitedLessons, { maxAge: 900000, httpOnly: true }) diff --git a/routes/index.js b/routes/index.js index 9b41ad9..5738d1f 100644 --- a/routes/index.js +++ b/routes/index.js @@ -5,7 +5,7 @@ const router = express.Router() /* GET the home landing page. */ router.get('/', catchErrors(async function (req, res, next) { - const landingPage = await getLandingPage('home', req.query.locale, req.query.api) + const landingPage = await getLandingPage('home', res.locals.currentLocale.code, res.locals.currentLocale.id) let title = landingPage.fields.title if (!title || landingPage.fields.slug === 'home') { title = 'The Example App' diff --git a/services/contentful.js b/services/contentful.js index 40350d9..8032cb1 100644 --- a/services/contentful.js +++ b/services/contentful.js @@ -23,6 +23,11 @@ exports.initClient = (options) => { }) } +exports.getSpace = assert((api = `cda`) => { + const client = api === 'cda' ? cdaClient : cpaClient + return client.getSpace() +}, 'Space') + exports.getCourses = assert((locale = 'en-US', api = `cda`) => { // to get all the courses we request all the entries // with the content_type `course` from Contentful diff --git a/views/layout.pug b/views/layout.pug index 2caedde..de2f133 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -30,17 +30,34 @@ html | View on Github .header__controls - form(action='' method='get') - .group - label(for='api') API: - select(name='api' onChange='this.form.submit()') - option(value='cda' selected=query.api === 'cda') Content Delivery API - option(value='cpa' selected=query.api === 'cpa') Content Preview API - .group - label(for='locale') Locale: - select(name='locale' onChange='this.form.submit()') - option(value='en-US' selected=query.locale === 'en-US') English - option(value='de-DE' selected=query.locale === 'de-DE') German + .header__controls_group + form(action='' method='get') + .header__controls_label API: #{currentApi.label} + .header__controls_dropdown + .header__controls_help_text View the published or draft content by simply switching between the Deliver and Preview APIs. + button.header__controls_button( + type='submit' + name='api' + value='cda' + class=`${currentApi.id === 'cda' ? 'header__controls_button--active' : ''}` + ) Delivery (published content) + button.header__controls_button( + type='submit' + name='api' + value='cpa' + class=`${currentApi.id === 'cpa' ? 'header__controls_button--active' : ''}` + ) Preview (draft content) + input(type='hidden' name='locale' value=currentLocale.code) + + .header__controls_group + form(action='' method='get') + input(type='hidden' name='api' value=currentApi.id) + .header__controls_label Locale: #{currentLocale.name} + .header__controls_dropdown + .header__controls_help_text Working with multiple languages? You can query the Content Delivery API for a specific locale. + each locale in locales + button.header__controls_button(type='submit' name='locale' value=locale.code class=`${locale.code === currentLocale.code ? 'header__controls_button--active' : ''}`)= `${locale.name} (${locale.code})` + .header__lower-wrapper .header__lower.layout-centered .header__logo @@ -93,4 +110,6 @@ html img(src='/images/icon-twitter.svg' alt='Our Twitter profile') script(src='/scripts/highlight.pack.js') script(src='/scripts/textFit.min.js') + script(src='/scripts/popper.min.js') + script(src='/scripts/hoverintent.min.js') script(src='/scripts/index.js')