From 112f495aba876f6194f0d29ceb14063fc8591d62 Mon Sep 17 00:00:00 2001 From: Misha Vyrtsev Date: Mon, 17 Jun 2019 05:26:02 +0300 Subject: [PATCH] Add email login support (#26) * add email login support * convert indentation to spaces * hide active forms on dev and github logins --- _example/frontend/index.html | 38 +- _example/frontend/main.js | 727 ++++++++++++++++++++++------------- _example/frontend/style.css | 168 ++++---- 3 files changed, 584 insertions(+), 349 deletions(-) diff --git a/_example/frontend/index.html b/_example/frontend/index.html index 7b6b3676..b1e777a8 100644 --- a/_example/frontend/index.html +++ b/_example/frontend/index.html @@ -1,26 +1,26 @@ - - - - GO-PKGZ/AUTH - + + + + GO-PKGZ/AUTH + -
-

GO-PKGZ/AUTHExample page

-

This library provides “social login” with Github, Google, Facebook and Yandex.

- -
-
Status: unauthorized
-
User info:
-
No info
-
-

Private page contents:

-

unauthorized

-
- - +
+

GO-PKGZ/AUTHExample page

+

This library provides “social login” with Github, Google, Facebook and Yandex.

+ +
+
Status: unauthorized
+
User info:
+
No info
+
+

Private page contents:

+

unauthorized

+
+ + \ No newline at end of file diff --git a/_example/frontend/main.js b/_example/frontend/main.js index ad6ad168..49265dc7 100644 --- a/_example/frontend/main.js +++ b/_example/frontend/main.js @@ -1,297 +1,510 @@ function getCookies() { - return document.cookie.split("; ").reduce((c, x) => { - const splitted = x.split("="); - c[splitted[0]] = splitted[1]; - return c; - }, {}); + return document.cookie.split("; ").reduce((c, x) => { + const splitted = x.split("="); + c[splitted[0]] = splitted[1]; + return c; + }, {}); } function req(endpoint, data = {}) { - const cloneData = Object.assign({}, data); - const cookies = getCookies(); - const token = cookies["XSRF-TOKEN"]; - - if (cloneData.hasOwnProperty("headers")) { - const headersClone = new Headers(cloneData.headers); - cloneData.headers = headersClone; - } else { - cloneData.headers = new Headers(); - } - - if (token) { - cloneData.headers.append("X-XSRF-TOKEN", token); - } - - return fetch(endpoint, cloneData).then(resp => { - if (resp.status >= 400) throw resp; - return resp.json().catch(() => null); - }); + const cloneData = Object.assign({}, data); + const cookies = getCookies(); + const token = cookies["XSRF-TOKEN"]; + + if (cloneData.hasOwnProperty("headers")) { + const headersClone = new Headers(cloneData.headers); + cloneData.headers = headersClone; + } else { + cloneData.headers = new Headers(); + } + + if (token) { + cloneData.headers.append("X-XSRF-TOKEN", token); + } + + return fetch(endpoint, cloneData).then(resp => { + if (resp.status >= 400) { + throw resp; + } + return resp.json().catch(() => null); + }); } function getProviders() { - return req("/auth/list"); + return req("/auth/list"); } function getUser() { - return req("/auth/user").catch(e => { - if (e.status && e.status === 401) return null; - throw e; - }); + return req("/auth/user").catch(e => { + if (e.status && e.status === 401) return null; + throw e; + }); } function login(prov) { - return new Promise((resolve, reject) => { - const url = window.location.href + "?close=true"; - const eurl = encodeURIComponent(url); - const win = window.open( - "/auth/" + prov + "/login?id=auth-example&from=" + eurl - ); - const interval = setInterval(() => { - try { - if (win.closed) { - reject(new Error("Login aborted")); - clearInterval(interval); - return; - } - if (win.location.search.indexOf("error") !== -1) { - reject(new Error(win.location.search)); - win.close(); - clearInterval(interval); - return; - } - if (win.location.href.indexOf(url) === 0) { - resolve(); - win.close(); - clearInterval(interval); - return; - } - } catch (e) {} - }, 100); - }); + return new Promise((resolve, reject) => { + const url = window.location.href + "?close=true"; + const eurl = encodeURIComponent(url); + const win = window.open( + "/auth/" + prov + "/login?id=auth-example&from=" + eurl + ); + const interval = setInterval(() => { + try { + if (win.closed) { + reject(new Error("Login aborted")); + clearInterval(interval); + return; + } + if (win.location.search.indexOf("error") !== -1) { + reject(new Error(win.location.search)); + win.close(); + clearInterval(interval); + return; + } + if (win.location.href.indexOf(url) === 0) { + resolve(); + win.close(); + clearInterval(interval); + return; + } + } catch (e) {} + }, 100); + }); } function loginAnonymously(username) { - return fetch( - `/auth/anonymous/login?id=auth-example&user=${encodeURIComponent(username)}` - ); + return fetch( + `/auth/anonymous/login?id=auth-example&user=${encodeURIComponent(username)}` + ); +} + +function sendEmailAuthData(username, email) { + return req( + `/auth/email/login?id=auth-example&user=${encodeURIComponent( + username + )}&address=${encodeURIComponent(email)}` + ); +} + +function loginViaEmailToken(token) { + return req(`/auth/email/login?token=${token}`); } const validUsernameRegex = /^[a-zA-Z][\w ]+$/; function getUsernameInvalidReason(username) { - if (username.length < 3) return "Username must be at least 3 characters long"; - if (!validUsernameRegex.test(username)) - return "Username must start from the letter and contain only latin letters, numbers, underscores, and spaces"; - return null; + if (username.length < 3) return "Username must be at least 3 characters long"; + if (!validUsernameRegex.test(username)) + return "Username must start from the letter and contain only latin letters, numbers, underscores, and spaces"; + return null; +} + +const validEmailRegex = /[^@]+@[^\.]+\..+/; + +function getEmailInvalidReason(email) { + if (!validEmailRegex.test(email)) { + return "Email should match /^[a-zA-Z][\\w ]+$/ regex"; + } + return null; +} + +function getTokenInvalidReason(token) { + if (token.length < 1) return "Token should be filled"; + return null; } function getAnonymousLoginForm(onSubmit) { - const form = document.createElement("form"); - - const input = document.createElement("input"); - input.type = "text"; - input.placeholder = "Username"; - input.className = "anon-form__input"; - - const submit = document.createElement("input"); - submit.type = "submit"; - submit.value = "Log in"; - submit.className = "anon-form__submit"; - - const onValueChange = val => { - const reason = getUsernameInvalidReason(val); - if (reason === null) { - submit.disabled = false; - submit.title = ""; - } else { - submit.disabled = true; - submit.title = reason; - } - }; - - onValueChange(input.value); - - input.addEventListener("input", e => { - const val = e.target.value; - const reason = getUsernameInvalidReason(val); - if (reason === null) { - submit.disabled = false; - submit.title = ""; - } else { - submit.disabled = true; - submit.title = reason; - } - }); - - form.appendChild(input); - form.appendChild(submit); - - form.addEventListener("submit", e => { - e.preventDefault(); - onSubmit(input.value); - }); - - return form; + const form = document.createElement("form"); + + const input = document.createElement("input"); + input.type = "text"; + input.placeholder = "Username"; + input.className = "anon-form__input"; + + const submit = document.createElement("input"); + submit.type = "submit"; + submit.value = "Log in"; + submit.className = "anon-form__submit"; + + const onValueChange = val => { + const reason = getUsernameInvalidReason(val); + if (reason === null) { + submit.disabled = false; + submit.title = ""; + } else { + submit.disabled = true; + submit.title = reason; + } + }; + + onValueChange(input.value); + + input.addEventListener("input", e => { + onValueChange(e.target.value); + }); + + form.appendChild(input); + form.appendChild(submit); + + form.addEventListener("submit", e => { + e.preventDefault(); + onSubmit(input.value); + }); + + return form; +} + +function getEmailLoginForm(onSubmit) { + const form = document.createElement("form"); + + const usernameInput = document.createElement("input"); + usernameInput.type = "text"; + usernameInput.placeholder = "Username"; + usernameInput.className = "email-form__input email-form__username-input"; + + const emailInput = document.createElement("input"); + emailInput.type = "text"; + emailInput.placeholder = "Email"; + emailInput.className = "email-form__input email-form__email-input"; + + const submit = document.createElement("input"); + submit.type = "submit"; + submit.value = "Log in"; + submit.className = "anon-form__submit"; + + const formValidation = ["Enter username", "Enter email"]; + + const onUserNameValueChange = val => { + const reason = getUsernameInvalidReason(val); + formValidation[0] = reason === null ? null : reason; + submit.title = formValidation.filter(x => x !== null).join("\n") || ""; + submit.disabled = submit.title.length > 0; + }; + + const onEmailValueChange = val => { + const reason = getEmailInvalidReason(val); + formValidation[1] = reason === null ? null : reason; + submit.title = formValidation.filter(x => x !== null).join("\n") || ""; + submit.disabled = submit.title.length > 0; + }; + + submit.title = formValidation.filter(x => x !== null).join("\n") || ""; + submit.disabled = submit.title.length > 0; + + usernameInput.addEventListener("input", e => { + onUserNameValueChange(e.target.value); + }); + + emailInput.addEventListener("input", e => { + onEmailValueChange(e.target.value); + }); + + form.appendChild(usernameInput); + form.appendChild(emailInput); + form.appendChild(submit); + + form.addEventListener("submit", e => { + e.preventDefault(); + onSubmit(usernameInput.value, emailInput.value); + }); + + form.reset = () => { + usernameInput.value = ""; + onUserNameValueChange(""); + emailInput.value = ""; + onEmailValueChange(""); + }; + + return form; +} + +function getEmailTokenLoginForm(onSubmit) { + const form = document.createElement("form"); + + const tokenInput = document.createElement("input"); + tokenInput.type = "text"; + tokenInput.placeholder = "Token"; + tokenInput.className = "email-form__input email-form__token-input"; + + const submit = document.createElement("input"); + submit.type = "submit"; + submit.value = "Submit"; + submit.className = "email-form__submit"; + + const onTokenValueChange = val => { + const reason = getTokenInvalidReason(val); + if (reason !== null) { + submit.title = reason; + submit.disabled = true; + } else { + submit.title = ""; + submit.disabled = false; + } + }; + + onTokenValueChange(tokenInput.value); + + tokenInput.addEventListener("input", e => { + onTokenValueChange(e.target.value); + }); + + form.appendChild(tokenInput); + form.appendChild(submit); + + form.addEventListener("submit", e => { + e.preventDefault(); + onSubmit(tokenInput.value); + }); + + form.reset = () => { + tokenInput.value = ""; + onTokenValueChange(""); + }; + + return form; +} + +function errorHandler(err) { + const status = document.querySelector(".status__label"); + if (err instanceof Response) { + err.text().then(text => { + try { + const data = JSON.parse(text); + if (data.error) { + status.textContent = data.error; + console.error(data.error); + return; + } + } catch {} + status.textContent = text; + console.error(text); + }); + return; + } + status.textContent = err.message; + console.error(err.message); } function getLoginLinks() { - return getProviders().then(providers => - providers.map(prov => { - let a; - if (prov === "anonymous") { - a = document.createElement("span"); - a.dataset.provider = prov; - a.className = "login__prov"; - - const textEl = document.createElement("span"); - textEl.textContent = "Login with " + prov; - textEl.className = "pseudo"; - a.appendChild(textEl); - textEl.addEventListener("click", e => { - form.style.display = form.style.display === "none" ? "block" : "none"; - form.querySelector(".anon-form__input").focus(); - }); - - const form = getAnonymousLoginForm(username => { - loginAnonymously(username) - .then(() => { - window.location.replace(window.location.href); - }) - .catch(e => { - const status = document.querySelector(".status__label"); - status.textContent = e.message; - }); - }); - form.style.display = "none"; - form.className = "anon-form login__anon-form"; - - a.appendChild(form); - } else { - a = document.createElement("span"); - a.dataset.provider = prov; - a.textContent = "Login with " + prov; - a.className = "pseudo login__prov"; - a.addEventListener("click", e => { - e.preventDefault(); - login(prov) - .then(() => { - window.location.replace(window.location.href); - }) - .catch(e => { - const status = document.querySelector(".status__label"); - status.textContent = e.message; - }); - }); - } - return a; - }) - ); + let formSwitcher = () => {}; + + return getProviders().then(providers => + providers.map(prov => { + let a; + if (prov === "anonymous") { + a = document.createElement("span"); + a.dataset.provider = prov; + a.className = "login__prov"; + + const textEl = document.createElement("span"); + textEl.textContent = "Login with " + prov; + textEl.className = "pseudo"; + a.appendChild(textEl); + textEl.addEventListener("click", e => { + const display = form.style.display; + formSwitcher(); + if (display === "none") { + form.style.display = "block"; + formSwitcher = () => { + form.style.display = "none"; + }; + form.querySelector(".anon-form__input").focus(); + } else { + form.style.display = "none"; + formSwitcher = () => {}; + } + }); + + const form = getAnonymousLoginForm(username => { + loginAnonymously(username) + .then(() => { + window.location.replace(window.location.href); + }) + .catch(errorHandler); + }); + form.style.display = "none"; + form.className = "anon-form login__anon-form"; + + a.appendChild(form); + } else if (prov === "email") { + a = document.createElement("span"); + a.dataset.provider = prov; + a.className = "login__prov"; + + const textEl = document.createElement("span"); + textEl.textContent = "Login with " + prov; + textEl.className = "pseudo"; + a.appendChild(textEl); + textEl.addEventListener("click", e => { + const diplay = formStage1.style.display; + formSwitcher(); + if (diplay === "none") { + formStage1.style.display = "block"; + formStage2.style.display = "block"; + formSwitcher = () => { + formStage1.style.display = "none"; + formStage2.style.display = "none"; + }; + formStage1.querySelector(".email-form__username-input").focus(); + } else { + formStage1.style.display = "none"; + formStage2.style.display = "none"; + formSwitcher = () => {}; + } + }); + + const formStage1 = getEmailLoginForm((username, email) => { + sendEmailAuthData(username, email) + .then(() => { + formStage1.classList.add("hidden"); + formStage2.classList.remove("hidden"); + formStage2.querySelector(".email-form__token-input").focus(); + }) + .catch(errorHandler); + }); + formStage1.style.display = "none"; + formStage1.className = "email-form login__email-form"; + a.appendChild(formStage1); + + const formStage2 = getEmailTokenLoginForm(token => { + loginViaEmailToken(token) + .then(() => { + window.location.replace(window.location.href); + }) + .catch(e => { + formStage1.classList.remove("hidden"); + formStage2.reset(); + formStage2.classList.add("hidden"); + errorHandler(e); + }); + }); + formStage2.style.display = "none"; + formStage2.className = "email-form login__email-form hidden"; + a.appendChild(formStage2); + } else { + a = document.createElement("span"); + a.dataset.provider = prov; + a.textContent = "Login with " + prov; + a.className = "pseudo login__prov"; + a.addEventListener("click", e => { + formSwitcher(); + e.preventDefault(); + login(prov) + .then(() => { + window.location.replace(window.location.href); + }) + .catch(errorHandler); + }); + } + return a; + }) + ); } function getLogoutLink() { - const a = document.createElement("a"); - a.href = "#"; - a.textContent = "Logout"; - a.className = "login__prov"; - a.addEventListener("click", e => { - e.preventDefault(); - req("/auth/logout") - .then(() => { - window.location.replace(window.location.href); - }) - .catch(e => console.error(e)); - }); - return a; + const a = document.createElement("a"); + a.href = "#"; + a.textContent = "Logout"; + a.className = "login__prov"; + a.addEventListener("click", e => { + e.preventDefault(); + req("/auth/logout") + .then(() => { + window.location.replace(window.location.href); + }) + .catch(errorHandler); + }); + return a; } function getUserInfoFragment(user) { - const table = document.createElement("table"); - table.className = "info__container"; - const imgtd = document.createElement("td"); - imgtd.rowSpan = Object.keys(user).length; - imgtd.className = "info__image-wide"; - const img = document.createElement("img"); - img.class = "info__user-image"; - img.src = user.picture; - imgtd.appendChild(img); - - { - const imgtr = document.createElement("tr"); - imgtr.className = "info__image-narrow"; - table.appendChild(imgtr); - const imgtd = document.createElement("td"); - imgtr.appendChild(imgtd); - imgtd.colSpan = 3; - const img = document.createElement("img"); - img.class = "info__user-image"; - img.src = user.picture; - imgtd.appendChild(img); - } - - let imgappended = false; - for (let key of Object.keys(user)) { - let tr = document.createElement("tr"); - if (!imgappended) { - tr.appendChild(imgtd); - imgappended = true; - } - let keytd = document.createElement("td"); - keytd.className = "info__key-cell"; - keytd.textContent = key; - - let valtd = document.createElement("td"); - valtd.className = "info__val-cell"; - if (typeof user[key] === "object") { - valtd.textContent = JSON.stringify(user[key]); - } else { - valtd.textContent = user[key]; - } - tr.appendChild(keytd); - tr.appendChild(valtd); - table.appendChild(tr); - } - return table; + const table = document.createElement("table"); + table.className = "info__container"; + const imgtd = document.createElement("td"); + imgtd.rowSpan = Object.keys(user).length; + imgtd.className = "info__image-wide"; + const img = document.createElement("img"); + img.class = "info__user-image"; + img.src = user.picture; + imgtd.appendChild(img); + + { + const imgtr = document.createElement("tr"); + imgtr.className = "info__image-narrow"; + table.appendChild(imgtr); + const imgtd = document.createElement("td"); + imgtr.appendChild(imgtd); + imgtd.colSpan = 3; + const img = document.createElement("img"); + img.class = "info__user-image"; + img.src = user.picture; + imgtd.appendChild(img); + } + + let imgappended = false; + for (let key of Object.keys(user)) { + let tr = document.createElement("tr"); + if (!imgappended) { + tr.appendChild(imgtd); + imgappended = true; + } + let keytd = document.createElement("td"); + keytd.className = "info__key-cell"; + keytd.textContent = key; + + let valtd = document.createElement("td"); + valtd.className = "info__val-cell"; + if (typeof user[key] === "object") { + valtd.textContent = JSON.stringify(user[key]); + } else { + valtd.textContent = user[key]; + } + tr.appendChild(keytd); + tr.appendChild(valtd); + table.appendChild(tr); + } + return table; } function main() { - if (window.location.search.indexOf("?close=true") !== -1) { - document.body.textContent = "Logged in!"; - return; - } - return getUser().then(user => { - const loginContainer = document.querySelector(".login"); - const statusElement = document.querySelector(".status__label"); - if (!user) { - getLoginLinks().then(links => { - for (let link of links) { - loginContainer.appendChild(link); - } - }); - return; - } - loginContainer.appendChild(getLogoutLink()); - statusElement.textContent = "logged in as " + user.name; - const infoEl = document.querySelector(".info"); - infoEl.textContent = ""; - infoEl.appendChild(getUserInfoFragment(user)); - - req("/private_data") - .then(data => { - data = JSON.stringify(data, null, " "); - const el = document.createElement("pre"); - el.textContent = data; - el.className = "protected-data__data"; - const container = document.querySelector(".protected-data"); - const placeholder = container.querySelector( - ".protected-data__placeholder" - ); - placeholder.remove(); - container.appendChild(el); - }) - .catch(e => console.error(e)); - }); + if (window.location.search.indexOf("?close=true") !== -1) { + document.body.textContent = "Logged in!"; + return; + } + return getUser().then(user => { + const loginContainer = document.querySelector(".login"); + const statusElement = document.querySelector(".status__label"); + if (!user) { + getLoginLinks().then(links => { + for (let link of links) { + loginContainer.appendChild(link); + } + }); + return; + } + loginContainer.appendChild(getLogoutLink()); + statusElement.textContent = "logged in as " + user.name; + const infoEl = document.querySelector(".info"); + infoEl.textContent = ""; + infoEl.appendChild(getUserInfoFragment(user)); + + req("/private_data") + .then(data => { + data = JSON.stringify(data, null, " "); + const el = document.createElement("pre"); + el.textContent = data; + el.className = "protected-data__data"; + const container = document.querySelector(".protected-data"); + const placeholder = container.querySelector( + ".protected-data__placeholder" + ); + placeholder.remove(); + container.appendChild(el); + }) + .catch(() => "access to /private_data denied"); + }); } main().catch(e => { - console.error(e); + console.error(e); }); diff --git a/_example/frontend/style.css b/_example/frontend/style.css index ed604b60..c47333cf 100644 --- a/_example/frontend/style.css +++ b/_example/frontend/style.css @@ -1,165 +1,187 @@ html { - font-family: Arial, Helvetica, sans-serif; - background: hsl(0, 0%, 99%); + font-family: Arial, Helvetica, sans-serif; + background: hsl(0, 0%, 99%); } a, .pseudo { - color: hsla(200, 60%, 50%, 1); - text-decoration-color: hsla(200, 60%, 50%, 0.5); + color: hsla(200, 60%, 50%, 1); + text-decoration-color: hsla(200, 60%, 50%, 0.5); } .pseudo { - cursor: pointer; - text-decoration: underline; + cursor: pointer; + text-decoration: underline; } a:hover, .pseudo:hover { - color: hsla(200, 80%, 70%, 1); - text-decoration-color: hsla(200, 80%, 70%, 0.5); + color: hsla(200, 80%, 70%, 1); + text-decoration-color: hsla(200, 80%, 70%, 0.5); } hr { - border: none; - border-top: 1px solid rgba(0, 0, 0, 0.04); + border: none; + border-top: 1px solid rgba(0, 0, 0, 0.04); +} + +.hidden { + display: none !important; } .minititle { - opacity: 0.6; + opacity: 0.6; } .main-header { - background: #fff; - padding: 0.5rem; - margin-top: -0.5rem; - margin-left: -0.5rem; - width: 100%; - margin-bottom: 1rem; - box-shadow: 0px 0px 0.15rem rgba(0, 0, 0, 0.07); + background: #fff; + padding: 0.5rem; + margin-top: -0.5rem; + margin-left: -0.5rem; + width: 100%; + margin-bottom: 1rem; + box-shadow: 0px 0px 0.15rem rgba(0, 0, 0, 0.07); } .pretitle { - font-size: 0.8rem; - opacity: 0.4; - margin-bottom: 2rem; - margin-left: 0.2rem; - font-weight: normal; - letter-spacing: normal; + font-size: 0.8rem; + opacity: 0.4; + margin-bottom: 2rem; + margin-left: 0.2rem; + font-weight: normal; + letter-spacing: normal; } .anon-form { - background: white; - padding: 0.5em; - z-index: 999; - box-shadow: 0 0 0.15rem rgba(0, 0, 0, 0.2); + background: white; + padding: 0.5em; + z-index: 999; + box-shadow: 0 0 0.15rem rgba(0, 0, 0, 0.2); } .anon-form__input { - min-width: 20em; + min-width: 20em; +} + +.email-form { + background: white; + padding: 0.5em; + z-index: 999; + box-shadow: 0 0 0.15rem rgba(0, 0, 0, 0.2); +} + +.email-form__input { + min-width: 20em; + display: block; } @media screen and (max-width: 34em) { - .pretitle { - display: block; - } + .pretitle { + display: block; + } } .title { - text-shadow: none; - font-size: 3rem; - letter-spacing: 0.02em; - margin-top: 0; - margin-bottom: 0; + text-shadow: none; + font-size: 3rem; + letter-spacing: 0.02em; + margin-top: 0; + margin-bottom: 0; } .description { - margin-top: 0; - opacity: 0.6; + margin-top: 0; + opacity: 0.6; } .title a:not(:hover) { - text-decoration: none; + text-decoration: none; } .login__prov { - margin-right: 0.4em; - position: relative; - display: inline-block; + margin-right: 0.4em; + position: relative; + display: inline-block; } .login__anon-form { - position: absolute; - top: 1.4em; - left: -0.5em; + position: absolute; + top: 1.4em; + left: -0.5em; +} + +.login__email-form { + position: absolute; + top: 1.4em; + left: -0.5em; } .info-label { - margin-top: 1em; + margin-top: 1em; } .info__container { - border-collapse: collapse; - font-size: 0.8em; - background: #fff; - width: 100%; + border-collapse: collapse; + font-size: 0.8em; + background: #fff; + width: 100%; } .info__container, .info__container tr, .info__container td { - border: 1px solid rgba(0, 0, 0, 0.04); + border: 1px solid rgba(0, 0, 0, 0.04); } .info__container, td { - vertical-align: top; - padding: 0.4em; + vertical-align: top; + padding: 0.4em; } .info__key-cell { - width: 2rem; + width: 2rem; } .info__val-cell { - word-break: break-all; + word-break: break-all; } .protected-data__title { - margin-bottom: 0; + margin-bottom: 0; } .protected-data__placeholder { - margin-top: 0; + margin-top: 0; } .protected-data__data { - margin-top: 0; - padding: 0.5rem; - background: #fff; - border: 1px solid rgba(0, 0, 0, 0.04); + margin-top: 0; + padding: 0.5rem; + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.04); } .info__image-wide { - width: 1px; + width: 1px; } .info__image-narrow { - text-align: center; - display: none; + text-align: center; + display: none; } @media screen and (max-width: 30em) { - .info__image-narrow { - display: table-row; - } + .info__image-narrow { + display: table-row; + } - .info__image-wide { - display: none; - } + .info__image-wide { + display: none; + } } .footer { - margin-top: 4rem; - color: rgba(0, 0, 0, 0.6); + margin-top: 4rem; + color: rgba(0, 0, 0, 0.6); }