From 6430a2c849e002d928e90bc9daeaae85042d218c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Wed, 4 Oct 2017 17:01:23 +0200 Subject: [PATCH] style forms and add form validation and form status block --- assets/stylesheets/base/formhack.css | 13 +++- assets/stylesheets/base/forms.css | 63 ++++++++++++++- assets/stylesheets/variables.css | 6 ++ public/images/icon-error.svg | 6 ++ public/images/icon-success.svg | 4 + public/stylesheets/style.css | 83 +++++++++++++++++--- routes/settings.js | 112 ++++++++++++++++++++++++++- views/settings.pug | 28 ++++++- 8 files changed, 291 insertions(+), 24 deletions(-) create mode 100644 public/images/icon-error.svg create mode 100644 public/images/icon-success.svg diff --git a/assets/stylesheets/base/formhack.css b/assets/stylesheets/base/formhack.css index 3993257..4cc8029 100644 --- a/assets/stylesheets/base/formhack.css +++ b/assets/stylesheets/base/formhack.css @@ -77,12 +77,13 @@ datalist { label { display: var(--fh-layout-display); - margin: var(--fh-layout-margin); + margin: var(--fh-layout-margin) 0 0; text-align: var(--fh-layout-text-align); font-weight: bold; + font-size: 0.875em; & + input { - margin-top: calc(var(--grid-gutter) * 0.5 * -1) + margin-top: calc(var(--grid-gutter) * 0.25) } } @@ -233,9 +234,13 @@ button[disabled] { input:focus, textarea:focus, select:focus, -option:focus, +option:focus { + border-color: var(--color-palette-blue); + color: var(--fh-font-color); +} + button:focus, -.cta:focus { +.cta:focus { background-color: var(--fh-button-hover-bg-color); color: var(--fh-button-hover-font-color); } diff --git a/assets/stylesheets/base/forms.css b/assets/stylesheets/base/forms.css index bbc6845..bc9e50f 100644 --- a/assets/stylesheets/base/forms.css +++ b/assets/stylesheets/base/forms.css @@ -1,10 +1,65 @@ -.form-item { +@block form-item { & + .form-item { margin-top: var(--grid-gutter); } - & .help-text { - font-size: 0.9em; + + & input { + margin-bottom: calc(var(--grid-gutter) * 0.25); + } + + @element help-text { + font-size: 0.875em; color: var(--color-text-grey); - margin-top: calc(var(--grid-gutter) * -1); + } + + @element error-message { + font-size: 0.875em; + color: var(--color-text-error); + + &:before { + content: ''; + display: inline-block; + background: url('/images/icon-error.svg') no-repeat left center; + background-size: contain; + width: 12px; + height: 12px; + padding-right: 0.3em; + } + } +} + +@block status-block { + display: flex; + margin: var(--grid-gutter) 0; + padding: calc(var(--grid-gutter) * 1); + border-radius: var(--border-radius); + + font-size: 0.875em; + + @modifier success { + background: var(--color-status-block-bg-success); + } + + @modifier error { + background: var(--color-status-block-bg-error); + } + + @element icon { + flex: 0 0 auto; + width: 24px; + height: 24px; + margin-right: calc(var(--grid-gutter) * 0.5); + } + + @element content { + flex: 1 1 auto; + } + + @element title { + font-weight: bold; + } + + @element message { + color: var(--color-status-block-message); } } diff --git a/assets/stylesheets/variables.css b/assets/stylesheets/variables.css index da45035..37013ef 100644 --- a/assets/stylesheets/variables.css +++ b/assets/stylesheets/variables.css @@ -13,11 +13,13 @@ --color-palette-blue: #5c9fef; --color-palette-blue-medium: #3c80cf; --color-palette-blue-dark: #3072be; + --color-palette-red: #cd3f39; --color-text-default: #2a3039; --color-text-grey: #8091a5; --color-text-dark-bg: #fff; --color-text-dark-bg-grey: #a9b9c0; + --color-text-error: var(--color-palette-red); --color-link-content: var(--color-palette-blue); @@ -33,6 +35,10 @@ --color-course-active: #536171; --color-course-card-description: #536171; + --color-status-block-bg-success: #f4fffb; + --color-status-block-bg-error: #fbe3e2; + --color-status-block-message: #536171; + --layout-sidebar-sidebar-width: 228px; --layout-sidebar-content-width: 732px; diff --git a/public/images/icon-error.svg b/public/images/icon-error.svg new file mode 100644 index 0000000..5cd9f5b --- /dev/null +++ b/public/images/icon-error.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/icon-success.svg b/public/images/icon-success.svg new file mode 100644 index 0000000..5c5a410 --- /dev/null +++ b/public/images/icon-success.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index 4a5eeab..9f49228 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -561,13 +561,14 @@ datalist { label { display: block; - margin: 22px 0; + margin: 22px 0 0 0; text-align: left; - font-weight: bold + font-weight: bold; + font-size: 0.875em } label + input { - margin-top: -11px; + margin-top: 5.5px; } /* Input & Textarea ------------------ */ @@ -719,9 +720,13 @@ button[disabled] { input:focus, textarea:focus, select:focus, -option:focus, +option:focus { + border-color: #5c9fef; + color: rgb(40, 40, 40); +} + button:focus, -.cta:focus { +.cta:focus { background-color: rgb(125, 178, 242); color: #fff; } @@ -749,10 +754,70 @@ input[type="reset"]:focus, margin-top: 22px; } -.form-item .help-text { - font-size: 0.9em; - color: #8091a5; - margin-top: -22px; +.form-item input { + margin-bottom: 5.5px; +} + +.form-item__help-text { + font-size: 0.875em; + color: #8091a5; +} + +.form-item__error-message { + font-size: 0.875em; + color: #cd3f39; +} + +.form-item__error-message:before { + content: ''; + display: inline-block; + background: url('/images/icon-error.svg') no-repeat left center; + background-size: contain; + width: 12px; + height: 12px; + padding-right: 0.3em; +} + +.status-block { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + margin: 22px 0; + padding: 22px; + border-radius: 4px; + + font-size: 0.875em; +} + +.status-block--success { + background: #f4fffb; +} + +.status-block--error { + background: #fbe3e2; +} + +.status-block__icon { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 24px; + height: 24px; + margin-right: 11px; +} + +.status-block__content { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.status-block__title { + font-weight: bold; +} + +.status-block__message { + color: #536171; } .layout-centered { diff --git a/routes/settings.js b/routes/settings.js index dc527b4..97bcd29 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -1,4 +1,5 @@ const express = require('express') +const { createClient } = require('contentful') const { catchErrors } = require('../handlers/errorHandlers') const router = express.Router() @@ -6,14 +7,117 @@ const router = express.Router() router.get('/', catchErrors(async function (req, res, next) { const cookie = req.cookies.theExampleAppSettings const settings = cookie || { cpa: '', cda: '', space: '' } - res.render('settings', { title: 'Settings', settings }) + res.render('settings', { + title: 'Settings', + settings, + errors: {}, + hasErrors: false, + success: false + }) })) /* POST settings page. */ router.post('/', catchErrors(async function (req, res, next) { - const settings = {space: req.body.space, cda: req.body.cda, cpa: req.body.cpa} - res.cookie('theExampleAppSettings', settings, { maxAge: 900000, httpOnly: true }) - res.render('settings', settings) + const errorList = [] + const { space, cda, cpa } = req.body + const settings = {space, cda, cpa} + + // Validate required fields. + if (!space) { + errorList.push({ + field: 'space', + message: 'This field is required' + }) + } + + if (!cda) { + errorList.push({ + field: 'cda', + message: 'This field is required' + }) + } + + if (!cpa) { + errorList.push({ + field: 'cpa', + message: 'This field is required' + }) + } + + // Validate space and CDA access token. + if (space && cda) { + try { + await createClient({ + space, + accessToken: cda + }).getSpace() + } catch (err) { + if (err.response.status === 401) { + errorList.push({ + field: 'cda', + message: 'Your Delivery API key is invalid.' + }) + } else if (err.response.status === 404) { + errorList.push({ + field: 'space', + message: 'This space does not exist.' + }) + } else { + errorList.push({ + field: 'cda', + message: `Something went wrong: ${err.response.data.message}` + }) + } + } + } + + // Validate space and CPA access token. + if (space && cpa) { + try { + await createClient({ + space, + accessToken: cpa, + host: 'preview.contentful.com' + }).getSpace() + } catch (err) { + if (err.response.status === 401) { + errorList.push({ + field: 'cpa', + message: 'Your Preview API key is invalid.' + }) + } else if (err.response.status === 404) { + // Already validated via CDA + } else { + errorList.push({ + field: 'cpa', + message: `Something went wrong: ${err.response.data.message}` + }) + } + } + } + + if (!errorList.length) { + res.cookie('theExampleAppSettings', settings, { maxAge: 900000, httpOnly: true }) + } + + // Generate error dictionary + const errors = errorList.reduce((errors, error) => { + return { + ...errors, + [error.field]: [ + ...(errors[error.field] || []), + error.message + ] + } + }, {}) + + res.render('settings', { + title: 'Settings', + settings, + errors, + hasErrors: errorList.length > 0, + success: errorList.length === 0 + }) })) module.exports = router diff --git a/views/settings.pug b/views/settings.pug index 18ace49..a92bbe2 100644 --- a/views/settings.pug +++ b/views/settings.pug @@ -5,21 +5,43 @@ block content h1= title p To query and get content using the APIs, client applications need to authenticate with both the Space ID and an access token. + if success + .status-block.status-block--success + img.status-block__icon(src='/images/icon-success.svg') + .status-block__content + .status-block__title Changes saved successfully! + + if hasErrors + .status-block.status-block--error + img.status-block__icon(src='/images/icon-error.svg') + .status-block__content + .status-block__title Error occurred + .status-block__message Some errors occurred. Please check the error messages next to the fields. + form(action=`/settings` method="POST" class="form") .form-item label(for="space") Space ID input(type="text" name="space" value=settings.space) - .help-text Some help text we still need to define + if 'space' in errors + each message in errors.space + .form-item__error-message= message + .form-item__help-text Some help text we still need to define .form-item label(for="cda") Delivery API key input(type="text" name="cda" value=settings.cda) - .help-text Some help text we still need to define + if 'cda' in errors + each message in errors.cda + .form-item__error-message= message + .form-item__help-text Some help text we still need to define .form-item label(for="cpa") Preview API key input(type="text" name="cpa" value=settings.cpa) - .help-text Some help text we still need to define + if 'cpa' in errors + each message in errors.cpa + .form-item__error-message= message + .form-item__help-text Some help text we still need to define .form-item input.cta(type="submit" value="Load settings")