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")