From 0168535380cabee1a25d4dbbf08b7fc6a2f722da Mon Sep 17 00:00:00 2001 From: Chris Frank Date: Sun, 18 Aug 2019 20:59:30 -0400 Subject: [PATCH 1/7] Serve inline assets on error pages --- layouts/partials/head.ejs | 13 +- package-lock.json | 14 +- package.json | 5 +- public/errors.css | 755 ++++++++++++++++++++++++++++++ server/routes/errors.js | 55 ++- server/utils.js | 17 + styles/errors.scss | 18 + styles/partials/core/_errata.scss | 3 + test/functional/pages.test.js | 11 + test/unit/assetToDataURI.test.js | 26 + test/unit/errors.test.js | 12 +- 11 files changed, 908 insertions(+), 21 deletions(-) create mode 100644 public/errors.css create mode 100644 styles/errors.scss create mode 100644 styles/partials/core/_errata.scss create mode 100644 test/unit/assetToDataURI.test.js diff --git a/layouts/partials/head.ejs b/layouts/partials/head.ejs index 00d8b169..a3372ad8 100644 --- a/layouts/partials/head.ejs +++ b/layouts/partials/head.ejs @@ -8,10 +8,13 @@ - - - - - + <% if (locals.inlineCSS) { %> + + <% } else { %> + + + + <% } %> + diff --git a/package-lock.json b/package-lock.json index 9e3c6321..eb9b8083 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5555,16 +5555,16 @@ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" }, "mime-db": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz", - "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", - "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { - "mime-db": "~1.29.0" + "mime-db": "1.40.0" } }, "minimatch": { diff --git a/package.json b/package.json index 237c7ada..4422730e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "js-yaml": "^3.13.1", "lodash": "^4.17.14", "md5": "^2.2.1", + "mime-types": "^2.1.24", "moment": "^2.24.0", "node-sass": "^4.12.0", "passport": "^0.4.0", @@ -50,7 +51,9 @@ }, "scripts": { "start": "node server/index.js", - "build": "node-sass --include-path custom/styles --include-path styles/partials --source-map public/css/style.css.map --source-map-contents styles/style.scss public/css/style.css", + "build:main": "node-sass --include-path custom/styles --include-path styles/partials --source-map public/css/style.css.map --source-map-contents styles/style.scss public/css/style.css", + "build:inline": "node-sass --include-path custom/styles --include-path styles/partials styles/errors.scss public/errors.css", + "build": "npm run build:main && npm run build:inline", "debug": "node -r dotenv/config --inspect --debug-brk server/index.js", "watch": "npm run build && concurrently \"nodemon --inspect=0.0.0.0 -r dotenv/config -e js,ejs server/index.js\" \"nodemon -e scss --watch styles --watch custom/styles -x npm run build\"", "test": "NODE_ENV=test mocha test/**/*.test.js -r ./test/utils/bootstrap.js --recursive --exit", diff --git a/public/errors.css b/public/errors.css new file mode 100644 index 00000000..837c9e21 --- /dev/null +++ b/public/errors.css @@ -0,0 +1,755 @@ +@import url("https://fonts.googleapis.com/css?family=Alfa+Slab+One|Open+Sans:400,400i,700,700i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext,vietnamese"); +body { + font-family: "Open Sans", sans-serif; + font-weight: 100; + margin: 0; + padding: 0; } + +pre { + white-space: pre-wrap; } + +h1, h2, h3, h4, h5, h6 { + font-family: "Open Sans", sans-serif; + margin-top: 45px; + margin-bottom: 20px; } + +a { + color: #0095ff; + text-decoration: none; } + a:hover { + text-decoration: underline; + outline: 0; } + +table { + border-collapse: collapse; + width: 100%; } + +th, td { + padding: 0.25rem 0.5rem; + text-align: left; + border: 1px solid #ccc; } + th p, td p { + margin: 0; } + +button:focus { + outline: 0; } + +.legibility { + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-feature-settings: "kern"; } + +.visually-hidden { + display: none; } + +.button { + font-size: 14px; + line-height: 18px; + font-weight: 300; + font-style: normal; + font-family: "Open Sans", sans-serif; + background-color: transparent; + border-radius: 3px; + transition: background-color 0.3s; + display: inline-block; + padding: 4px 8px 6px 8px; + border: 1px solid #000; + color: #000; + cursor: pointer; } + @media (min-width: 768px) and (max-width: 1023px) { + .button { + font-size: 15px; + line-height: 20px; + padding: 5px 10px; } } + @media (min-width: 1024px) { + .button { + font-size: 15px; + line-height: 20px; + padding: 5px 10px; } } + +.button.btn-cat { + background: #fff; + border-radius: 0; + margin: 4px 0 0 0; + -webkit-font-smoothing: antialiased; } + @media (min-width: 768px) and (max-width: 1023px) { + .button.btn-cat { + margin: 3px 0 0 0; } } + @media (min-width: 1024px) { + .button.btn-cat { + margin: 3px 0 0 0; } } + .button.btn-cat:hover { + cursor: pointer; + background: #0095ff; + color: #000; + text-decoration: none; } + +.button.btn-homepage { + margin: 10px 0 0 0; + -webkit-font-smoothing: antialiased; + font-size: 14px; + background-color: #fff; + border: 1px solid #000; + border-radius: 0; + color: #aaa; + -webkit-box-shadow: 2.5px 2.5px 0px -1px #0068b3; + -moz-box-shadow: 2.5px 2.5px 0px -1px #0068b3; + box-shadow: 2.5px 2.5px 0px -1px #0068b3; + padding: 5px 12px; } + .button.btn-homepage:hover { + background: #0095ff; + color: #fff; + -webkit-box-shadow: 3px 3px 0px 0px #444; + -moz-box-shadow: 3px 3px 0px 0px #444; + box-shadow: 3px 3px 0px 0px #444; + text-decoration: none; } + +.button.btn-footer { + font-size: 15px; + line-height: 20px; + font-weight: 400; + font-style: normal; + font-family: "Open Sans", sans-serif; + background-color: transparent; + transition: background-color 0.3s; + display: inline-block; + padding: 7px 10px; + border: 1px solid #999; + cursor: pointer; + -webkit-font-smoothing: antialiased; + -webkit-box-shadow: 5px 5px 0px 0px #ececec; + -moz-box-shadow: 5px 5px 0px 0px #ececec; + box-shadow: 5px 5px 0px 0px #ececec; + margin-right: 10px; + color: #333; + margin-top: 10px; } + .button.btn-footer:hover { + background: #333; + color: #fff; } + +.button.btn-plaintext { + border: none; + text-decoration: underline; + background: transparent; + color: #999; + font-size: 13px; + line-height: 29px; } + .button.btn-plaintext:hover { + cursor: pointer; + color: #0095ff; } + +.masthead { + position: fixed; + background: #222; + top: 0; + left: 0; + width: 100vw; + z-index: 1000000090; + color: #ccc; } + .masthead .container { + position: relative; + min-height: 49px; } + .masthead .container .branding { + padding-top: 3px; + margin-bottom: 0px; + background: transparent; + float: left; } + .masthead .container .branding .branding-heading { + margin-top: 7px; + left: 20px; + position: absolute; } + .masthead .container .branding .branding-heading .logo { + height: 23px; + filter: invert(100%) brightness(80%); + margin-left: -5px; } + .masthead .container .branding .branding-label { + margin: 5px auto auto 62px; + display: inline-block; + height: 28px; + padding: 4px 0 0 15px; + vertical-align: top; + border-left: 1px solid rgba(255, 255, 255, 0.35); + color: #000; + font-family: "Alfa Slab One", cursive; + font-size: 18px; + text-transform: none; + -webkit-font-smoothing: antialiased; + font-weight: 100; + line-height: 26px; } + .masthead .container .branding .branding-label a:link, .masthead .container .branding .branding-label a:visited { + color: #ccc; } + .masthead .container .branding .branding-label a:hover { + color: #0095ff; + cursor: pointer; + text-decoration: none; } + .masthead .container .user-tools { + text-align: right; + right: 10px; + float: none; + position: fixed; + top: 9px; } + @media (min-width: 768px) and (max-width: 1023px) { + .masthead .container .user-tools { + right: 20px; } } + @media (min-width: 1024px) { + .masthead .container .user-tools { + right: 20px; } } + +.breadcrumb { + padding-left: 8px; + float: left; + margin: 15px 0; + display: none; } + @media (min-width: 768px) and (max-width: 1023px) { + .breadcrumb { + display: block; } } + @media (min-width: 1024px) { + .breadcrumb { + display: block; } } + +.breadcrumb-item { + display: inline-block; + font-size: 15px; + letter-spacing: 0.01em; + line-height: 17px; } + .breadcrumb-item a:hover { + text-decoration: underline; + outline: 0; } + .breadcrumb-item a:visited { + color: #80caff; } + .breadcrumb-item a:link { + text-decoration: none; + color: #80caff; } + +.search-container { + vertical-align: middle; + white-space: nowrap; } + @media (min-width: 768px) and (max-width: 1023px) { + .search-container { + width: calc(100% - 20px); } } + @media (min-width: 1024px) { + .search-container { + width: calc(100% - 20px); } } + .search-container #search-box { + -webkit-appearance: none; + border-radius: 0; + font-size: 16px; + line-height: 30px; + font-weight: 400; + font-style: normal; + font-family: "Open Sans", sans-serif; + background-color: transparent; + transition: background-color 0.3s; + display: inline-block; + padding: 7px 10px; + border: 1px solid #999; + -webkit-font-smoothing: antialiased; + -webkit-box-shadow: 5px 5px 0px 0px #444; + -moz-box-shadow: 5px 5px 0px 0px #444; + box-shadow: 5px 5px 0px 0px #444; + color: #000; + float: left; + padding-left: 15px; + width: calc(100% - 20px); + -webkit-transition: background .55s ease; + -moz-transition: background .55s ease; + -ms-transition: background .55s ease; + -o-transition: background .55s ease; + transition: background .55s ease; } + .search-container #search-box:hover, .search-container #search-box:focus, .search-container #search-box:active { + outline: none; } + .search-container:hover, .search-container:focus, .search-container:active { + outline: none; + background: #FCF6F6; } + .search-container:hover .icon, .search-container:focus .icon, .search-container:active .icon { + outline: none; + opacity: 1; } + .search-container .icon { + border: none; + background: #0095ff; + height: 46px; + width: 50px; + color: #fff; + opacity: 0; + font-size: 12pt; + cursor: pointer; + margin-left: -50px; + -webkit-transition: opacity .35s ease; + -moz-transition: opacity .35s ease; + -ms-transition: opacity .35s ease; + -o-transition: opacity .35s ease; + transition: opacity .35s ease; } + .search-container:hover .icon:hover { + background: #0095ff; } + .search-container input::-webkit-input-placeholder, + .search-container input:-moz-placeholder, + .search-container input:-ms-input-placeholder { + color: #aaa; } + .search-container input:focus::-webkit-input-placeholder, + .search-container input:focus:-moz-placeholder, + .search-container input:focus:-ms-input-placeholder { + opacity: 0; } + +.additional-menus { + clear: both; + margin-top: 5px; + display: flex; + justify-content: space-between; } + +#user-profile a.btn-user-whole:link, #user-profile a.btn-user-whole:visited { + color: #ccc; + font-size: 15px; + letter-spacing: 0.25px; + padding: 17px 15px 16px 15px; } + +#user-profile a.btn-user-whole:hover, #user-profile a.btn-user-whole:active, #user-profile a.btn-user-whole:target { + text-decoration: none; + background: gray; } + #user-profile a.btn-user-whole:hover button.btn-user-initial, #user-profile a.btn-user-whole:active button.btn-user-initial, #user-profile a.btn-user-whole:target button.btn-user-initial { + background: #000; + color: #ccc; + border-color: #666; } + +#user-profile .user-fullname { + margin-right: 5px; + display: none; } + @media (min-width: 768px) and (max-width: 1023px) { + #user-profile .user-fullname { + display: inline-block; } } + @media (min-width: 1024px) { + #user-profile .user-fullname { + display: inline-block; } } + +#user-profile button.btn-user-initial { + border-radius: 50%; + width: 32px; + height: 32px; + position: relative; + font-size: 14px; + -webkit-font-smoothing: antialiased; + text-indent: -1px; + background: #ccc; + color: #666; + margin-right: 5px; + line-height: 0px; + text-align: center; + border-color: #f3f3f3; + padding: 0; } + +.overlay { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + transition: opacity 100ms; + visibility: hidden; + opacity: 0; + text-align: left; } + .overlay .cancel { + position: fixed; + width: 100vw; + height: 100vh; + cursor: default; + top: 0; + left: 0; + right: 0; + bottom: 0; } + .overlay:target { + visibility: visible; + opacity: 1; } + +.popup { + margin-top: 40px; + padding: 0 20px 5px 20px; + background-image: linear-gradient(215deg, #fff 0%, #ececec 100%); + border: 1px solid #d4d4d4; + width: 260px; + float: right; + position: relative; } + @media (min-width: 768px) and (max-width: 1023px) { + .popup { + width: 300px; + padding: 20px; } } + @media (min-width: 1024px) { + .popup { + width: 300px; + padding: 20px; } } + .popup .close { + position: absolute; + width: 20px; + height: 20px; + top: 5px; + right: 5px; + opacity: 0.8; + transition: all 200ms; + font-size: 24px; + font-weight: bold; + text-decoration: none; + color: #666; } + .popup .close:hover { + opacity: 1; } + .popup h3 { + margin: 10px auto 0px auto; + color: #333; + border-bottom: 2px solid #333; + font-size: 16px; + line-height: 30px; + -webkit-font-smoothing: antialiased; } + @media (min-width: 768px) and (max-width: 1023px) { + .popup h3 { + margin: 10px auto; + line-height: 32px; } } + @media (min-width: 1024px) { + .popup h3 { + margin: 10px auto; + line-height: 32px; } } + .popup ul.recently-viewed-content, .popup ul.most-viewed-content { + margin: 0; + padding: 0; + list-style: none; } + .popup ul.recently-viewed-content li, .popup ul.most-viewed-content li { + margin: 0px 0 5px 0; + font-size: 15px; } + .popup ul.recently-viewed-content li a, .popup ul.most-viewed-content li a { + color: #333; } + .popup ul.recently-viewed-content li a:hover, .popup ul.most-viewed-content li a:hover { + text-decoration: none; } + .popup ul.recently-viewed-content li a:hover p, .popup ul.most-viewed-content li a:hover p { + color: #000; } + @media (min-width: 768px) and (max-width: 1023px) { + .popup ul.recently-viewed-content li, .popup ul.most-viewed-content li { + margin: 13px 0; } } + @media (min-width: 1024px) { + .popup ul.recently-viewed-content li, .popup ul.most-viewed-content li { + margin: 13px 0; } } + .popup ul.recently-viewed-content li .docs-title, .popup ul.most-viewed-content li .docs-title { + color: #0095ff; + margin-bottom: 2px; } + .popup ul.recently-viewed-content li .docs-attr, .popup ul.most-viewed-content li .docs-attr { + margin: 0; } + .popup ul.recently-viewed-content li .docs-folder, .popup ul.most-viewed-content li .docs-folder { + display: inline-block; + margin-right: 4px; + background: #ccc; + color: #666; + padding: 3px 4px; + font-weight: 400; + font-size: 12px; + border-radius: 3px; } + .popup ul.recently-viewed-content li .docs-folder:empty, .popup ul.most-viewed-content li .docs-folder:empty { + display: none; } + .popup ul.recently-viewed-content li .timestamp, .popup ul.most-viewed-content li .timestamp { + font-size: 12px; + color: #666; } + .popup .fa-spinner { + width: 100%; + margin: 20px 0; + font-size: 32px; + text-align: center; } + +#main-search-page { + background: #fff; + padding: 85px 20px 3px 20px; + min-height: calc(100vh - 88px); } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page { + padding: 85px 0px 3px 0px; + min-height: calc(100vh - 88px); } } + @media (min-width: 1024px) { + #main-search-page { + padding: 85px 0px 3px 0px; + min-height: calc(100vh - 88px); } } + #main-search-page .mid-center { + max-width: 640px; + margin: 0 auto; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item { + max-width: 600px; + margin: 30px auto 0 auto; } } + @media (min-width: 1024px) { + #main-search-page .main-item { + max-width: 600px; + margin: 30px auto 0 auto; } } + #main-search-page .main-item .branding { + display: flex; + justify-content: center; + align-content: center; + margin-bottom: 20px; } + #main-search-page .main-item .branding .branding-heading { + margin: 0; } + #main-search-page .main-item .branding .branding-heading .logo { + height: 26px; + padding-top: 9px; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item .branding .branding-heading .logo { + height: 36px; } } + @media (min-width: 1024px) { + #main-search-page .main-item .branding .branding-heading .logo { + height: 36px; } } + #main-search-page .main-item .branding .branding-label { + margin: 0 0 0 15px; + height: 21px; + padding: 10px 0 10px 15px; + border-left: 1px solid #000; + color: #000; + font-family: "Alfa Slab One", cursive; + font-size: 28px; + font-weight: 100; + line-height: 21px; + text-transform: none; + vertical-align: top; + -webkit-font-smoothing: antialiased; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item .branding .branding-label { + height: 30px; + padding: 15px 0 9px 18px; + font-size: 38px; + line-height: 26px; } } + @media (min-width: 1024px) { + #main-search-page .main-item .branding .branding-label { + height: 30px; + padding: 15px 0 10px 20px; + font-size: 38px; + line-height: 26px; } } + #main-search-page .main-item .branding .branding-label a:link, #main-search-page .main-item .branding .branding-label a:visited { + color: #ccc; } + #main-search-page .main-item .branding .branding-label a:hover { + color: #0095ff; + text-decoration: none; } + #main-search-page .main-item .tagline { + text-align: center; + color: #8e8e8e; + margin: 0 auto; + margin-bottom: 60px; + font-size: 14px; + line-height: 21px; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item .tagline { + font-size: 15px; + width: 100%; + margin-bottom: 10vh; } } + @media (min-width: 1024px) { + #main-search-page .main-item .tagline { + font-size: 15px; + width: 100%; + margin-bottom: 10vh; } } + #main-search-page .main-item .search-container { + clear: both; + background: #e6e2e2; + padding: 0px 6px 0 0; } + @media (min-width: 1024px) { + #main-search-page .main-item .search-container { + width: 586px; } } + #main-search-page .main-item .search-container #search-box { + font-size: 18px; + line-height: 30px; + float: none; + border: 0; + box-shadow: none; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item .search-container #search-box { + width: calc(100% - 20px); + box-shadow: 6px 6px 0px 0px #444; + line-height: 37px; } } + @media (min-width: 1024px) { + #main-search-page .main-item .search-container #search-box { + width: calc(100% - 20px); + box-shadow: 6px 6px 0px 0px #444; + line-height: 37px; } } + #main-search-page .main-item .search-container:hover, #main-search-page .main-item .search-container:focus, #main-search-page .main-item .search-container:active { + background: #ebebeb; } + #main-search-page .main-item .search-container:hover .icon, #main-search-page .main-item .search-container:focus .icon, #main-search-page .main-item .search-container:active .icon { + opacity: 1; } + #main-search-page .main-item .search-container .icon { + height: 51px; + width: 55px; + font-size: 15pt; + margin-left: -59px; + opacity: 0.95; + -webkit-transition: opacity .35s ease; + -moz-transition: opacity .35s ease; + -ms-transition: opacity .35s ease; + -o-transition: opacity .35s ease; + transition: opacity .35s ease; } + #main-search-page .main-item .search-container:hover .icon:hover { + background: #0095ff; } + #main-search-page .main-item .additional-menus { + justify-content: center; + margin-top: 30px; } + @media (min-width: 1024px) { + #main-search-page .main-item .additional-menus { + margin-top: 40px; } } + #main-search-page .main-item .additional-menus.homepage { + display: none; } + #main-search-page .main-item .featured-cat { + margin-top: 40px; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item .featured-cat { + margin-top: 30px; + display: flex; } } + @media (min-width: 1024px) { + #main-search-page .main-item .featured-cat { + margin-top: 30px; + display: flex; } } + #main-search-page .main-item .featured-cat .cat-container { + flex: 0 50%; + margin-bottom: 40px; } + #main-search-page .main-item .featured-cat .cat-container h3 { + font-weight: normal; + font-size: 14px; + color: #8e8e8e; + margin: 15px 0; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item .featured-cat .cat-container h3 { + font-size: 15px; } } + @media (min-width: 1024px) { + #main-search-page .main-item .featured-cat .cat-container h3 { + font-size: 15px; } } + #main-search-page .main-item .featured-cat .cat-container ul { + padding-left: 0; + list-style-type: none; } + #main-search-page .main-item .featured-cat .cat-container.style-button ul { + margin-bottom: 25px; } + #main-search-page .main-item .featured-cat .cat-container.style-button ul li { + display: inline-block; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item .featured-cat .cat-container.style-button ul li { + display: block; } } + @media (min-width: 1024px) { + #main-search-page .main-item .featured-cat .cat-container.style-button ul li { + display: block; } } + #main-search-page .main-item .featured-cat .cat-container.style-docs ul { + padding-left: 7px; } + #main-search-page .main-item .featured-cat .cat-container.style-docs ul a { + color: #000; + -webkit-font-smoothing: antialiased; } + #main-search-page .main-item .featured-cat .cat-container.style-docs ul li { + margin-bottom: 7px; + position: relative; } + #main-search-page .main-item .featured-cat .cat-container.style-docs ul li:last-of-type:before { + display: none; } + #main-search-page .main-item .featured-cat .cat-container.style-docs ul li .fa { + color: #000; + margin-right: 7px; + font-size: 14px; + line-height: 18px; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item--bottom { + top: 20px; + max-width: 100%; + padding-bottom: 20px; } } + @media (min-width: 1024px) { + #main-search-page .main-item--bottom { + top: 20px; + max-width: 100%; + padding-bottom: 20px; } } + #main-search-page .main-item--bottom .help-text { + color: #8e8e8e; + font-size: 14px; + line-height: 21px; + letter-spacing: 0; + text-align: left; } + #main-search-page .main-item--bottom .help-text a { + color: #8e8e8e; + border-bottom: 1px solid #8e8e8e; + padding-bottom: 1px; } + #main-search-page .main-item--bottom .help-text a:hover { + color: #ccc; + border-bottom: 1px solid #ccc; + text-decoration: none; } + @media (min-width: 768px) and (max-width: 1023px) { + #main-search-page .main-item--bottom .help-text { + line-height: 18px; + letter-spacing: 0.01em; + text-align: center; } } + @media (min-width: 1024px) { + #main-search-page .main-item--bottom .help-text { + line-height: 18px; + letter-spacing: 0.01em; + text-align: center; } } + +#category-page { + max-width: 90%; } + #category-page .g-main-content { + max-width: 100%; + display: flex; + flex-flow: row wrap; + border-top: 0; + justify-content: center; } + #category-page .g-main-content .children-view { + padding-left: 10px; } + #category-page .g-main-content .children-view.hide { + padding-left: 10px; + max-height: 400px; + overflow-y: hidden; } + #category-page .g-main-content .seeMore-button { + margin: 10px 0 0 0; + -webkit-font-smoothing: antialiased; + font-size: 14px; + background-color: white; + border: 1px solid #404040; + border-radius: 0; + color: black; + -webkit-box-shadow: 2.5px 2.5px 0px -1px #444; + -moz-box-shadow: 2.5px 2.5px 0px -1px #444; + box-shadow: 2.5px 2.5px 0px -1px #444; + padding: 5px 12px; + cursor: pointer; } + #category-page .g-main-content .seeMore-button:hover { + background-color: #ddd; } + #category-page .g-main-content .children-container { + overflow: visible; + margin: 0 12px 30px 12px; + padding: 25px; + border: 1px solid #ddd; + width: 100%; + -webkit-box-shadow: -4px -4px 0px 0px #efefef; + -moz-box-shadow: -4px -4px 0px 0px #efefef; + box-shadow: -4px -4px 0px 0px #efefef; } + @media (min-width: 768px) and (max-width: 1023px) { + #category-page .g-main-content .children-container { + width: 40.9%; } } + @media (min-width: 960px) and (max-width: 1023px) { + #category-page .g-main-content .children-container { + width: 34.9%; } } + @media (min-width: 1024px) { + #category-page .g-main-content .children-container { + width: 25.4%; } } + @media (min-width: 1600px) { + #category-page .g-main-content .children-container { + width: 18.4%; } } + #category-page .g-main-content .children-container h3 { + font-weight: 300; + font-size: 24px; + margin-top: 0; + margin-left: -5px; } + #category-page .g-main-content .children-container ol, #category-page .g-main-content .children-container ul { + margin: 0; + padding-left: 0px; + column-count: 1; + padding-left: 20px; } + #category-page .g-main-content .children-container ol li, #category-page .g-main-content .children-container ul li { + text-indent: 0; + font-size: 16px; + line-height: 1.6; + padding-right: 0; } + #category-page .g-main-content .children-container ol li a, #category-page .g-main-content .children-container ul li a { + color: #000; } + +#move-file-page li { + text-indent: 0; + display: block; } + +#move-file-page .folder i { + color: #336f99; } + +#move-file-page .folder.active > i { + color: #000; } + +.playlist-navigation { + display: flex; + justify-content: space-between; } + +.masthead .user-tools { + display: none; } diff --git a/server/routes/errors.js b/server/routes/errors.js index a24d560a..1b58fe43 100644 --- a/server/routes/errors.js +++ b/server/routes/errors.js @@ -1,7 +1,52 @@ 'use strict' +const fs = require('fs') const log = require('../logger') -const {stringTemplate} = require('../utils') +const {stringTemplate, assetDataURI} = require('../utils') + +let assetCache + +// Because asset requests are authenticated, we inline the images and CSS +// that we need to serve to logged-out users, e.g. on the auth error page. +function loadInlineAssets() { + if (assetCache) return new Promise((resolve) => resolve(assetCache)) + + const assets = {} + const assetPromises = ['branding.icon', 'branding.favicon'].map((key) => { + // Load essential images as base64 data-URIs + return new Promise((resolve, reject) => { + assetDataURI(stringTemplate(key)).then((img) => { + assets[key] = img + resolve() + }).catch((err) => { + // It's okay for users to have deleted these keys, + // but other errors should throw + if (err.code === 'ENOENT') { resolve(err) } else { throw err } + }) + }) + }).concat( + // Load the core stylesheet + new Promise((resolve, reject) => { + fs.readFile('public/errors.css', (err, data) => { + if (err) throw err + assets.css = data.toString() + resolve() + }) + }) + ) + + return new Promise((resolve, reject) => { + Promise.all(assetPromises).then(() => { + assets.stringTemplate = (key, ...args) => { + return assets[key] || stringTemplate(key, ...args) + } + // Store the inlined assets in memory for subsequent requests + assetCache = assets + + resolve(assetCache) + }) + }) +} // generic error handler to return error pages to user module.exports = (err, req, res, next) => { @@ -12,5 +57,11 @@ module.exports = (err, req, res, next) => { const code = messages[err.message] || 500 log.error(`Serving an error page for ${req.url}`, err) - res.status(code).render(`errors/${code}`, {err, template: stringTemplate}) + return loadInlineAssets().then((inline) => { + res.status(code).render(`errors/${code}`, { + inlineCSS: inline.css, + err, + template: inline.stringTemplate + }) + }) } diff --git a/server/utils.js b/server/utils.js index 071e2313..bcd06110 100644 --- a/server/utils.js +++ b/server/utils.js @@ -4,6 +4,7 @@ const path = require('path') const yaml = require('js-yaml') const { get: deepProp } = require('lodash') const merge = require('deepmerge') +const mime = require('mime-types') const log = require('./logger') @@ -101,3 +102,19 @@ exports.stringTemplate = (configPath, ...args) => { return '' } + +exports.assetDataURI = (filePath) => { + // If the path starts with `/assets`, look in the app’s public directory + const publicPath = filePath.replace(/^\/assets/, '/public') + + const mimeType = mime.lookup(path.posix.basename(publicPath)) + const fullPath = path.join(__dirname, '..', publicPath) + + return new Promise((resolve, reject) => { + fs.readFile(fullPath, { encoding: 'base64' }, (err, data) => { + if (err) reject(err) + const src = `data:${mimeType};base64,${data}` + resolve(src) + }) + }) +} diff --git a/styles/errors.scss b/styles/errors.scss new file mode 100644 index 00000000..8e4568ef --- /dev/null +++ b/styles/errors.scss @@ -0,0 +1,18 @@ +// When serving error pages, the app inlines the styles below instead of +// including the main stylesheet via a tag. This makes it possible to +// style error pages for logged-out visitors. (The main stylesheet is behind +// the authentication layer.) + +// top level templating values that can be overridden +@import "vars"; +@import "theme"; + +// core layout styles rarely overridden +@import "core/mixins"; +@import "core/base"; +@import "core/furniture"; +@import "core/pages"; +@import "core/errata"; + +// final stylesheet with any additional styles for custom logic +@import "custom"; diff --git a/styles/partials/core/_errata.scss b/styles/partials/core/_errata.scss new file mode 100644 index 00000000..5b4b2508 --- /dev/null +++ b/styles/partials/core/_errata.scss @@ -0,0 +1,3 @@ +.masthead .user-tools { + display: none; +} diff --git a/test/functional/pages.test.js b/test/functional/pages.test.js index 480791c6..b2e7282f 100644 --- a/test/functional/pages.test.js +++ b/test/functional/pages.test.js @@ -108,5 +108,16 @@ describe('Server responses', () => { ) }) }) + + it('should render an inline