diff --git a/src/components/ui/bsod/BSoD.intl.json b/src/components/ui/bsod/BSoD.intl.json new file mode 100644 index 0000000..f195005 --- /dev/null +++ b/src/components/ui/bsod/BSoD.intl.json @@ -0,0 +1,5 @@ +{ + "criticalErrorHappened": "There was a critical error due to which the application can not continue its normal operation.", + "reloadPageOrContactUs": "Please reload this page and try again. If problem occurs again, please report it to the developers by sending email to", + "alsoYouCanInteractWithBackground": "You can also play around with the background – it's interactable ;)" +} diff --git a/src/components/ui/bsod/BSoD.jsx b/src/components/ui/bsod/BSoD.jsx index 256625c..c8917e5 100644 --- a/src/components/ui/bsod/BSoD.jsx +++ b/src/components/ui/bsod/BSoD.jsx @@ -1,7 +1,255 @@ import React from 'react'; -export default function BSoD() { +import { FormattedMessage as Message } from 'react-intl'; + +import { IntlProvider } from 'components/i18n'; +import appInfo from 'components/auth/appInfo/AppInfo.intl.json'; +import messages from './BSoD.intl.json'; +import { rAF as requestAnimationFrame } from 'functions'; + +import styles from './styles.scss'; + +// TODO: probably it is better to render this view from the App view +// to remove dependencies from store and IntlProvider +export default function BSoD({store}) { return ( - + +
+ new BoxesField(el)} /> + +
+
+ +
+
+ +
+
+ +
+ + support@ely.by + +
+ +
+
+
+
); } + +class Box { + + constructor({size, startX, startY, startRotate, color, shadowColor}) { + this.color = color; + this.shadowColor = shadowColor; + this.setSize(size); + this.x = startX; + this.y = startY; + this.angle = startRotate; + this.shadowLength = 2000; // TODO: should be calculated + } + + get size() { + return this._initialSize; + } + + get dots() { + const full = (Math.PI * 2) / 4; + + const p1 = { + x: this.x + this.halfSize * Math.sin(this.angle), + y: this.y + this.halfSize * Math.cos(this.angle) + }; + + const p2 = { + x: this.x + this.halfSize * Math.sin(this.angle + full), + y: this.y + this.halfSize * Math.cos(this.angle + full) + }; + + const p3 = { + x: this.x + this.halfSize * Math.sin(this.angle + full * 2), + y: this.y + this.halfSize * Math.cos(this.angle + full * 2) + }; + + const p4 = { + x: this.x + this.halfSize * Math.sin(this.angle + full * 3), + y: this.y + this.halfSize * Math.cos(this.angle + full * 3) + }; + + return { p1, p2, p3, p4 }; + } + + rotate() { + const speed = (60 - this.halfSize) / 20; + this.angle += speed * 0.002; + this.x += speed; + this.y += speed; + } + + draw(ctx) { + const dots = this.dots; + ctx.beginPath(); + ctx.moveTo(dots.p1.x, dots.p1.y); + ctx.lineTo(dots.p2.x, dots.p2.y); + ctx.lineTo(dots.p3.x, dots.p3.y); + ctx.lineTo(dots.p4.x, dots.p4.y); + ctx.fillStyle = this.color; + ctx.fill(); + } + + drawShadow(ctx, light) { + const dots = this.dots; + const angles = []; + const points = []; + + for (const i in dots) { + const dot = dots[i]; + const angle = Math.atan2(light.y - dot.y, light.x - dot.x); + const endX = dot.x + this.shadowLength * Math.sin(-angle - Math.PI / 2); + const endY = dot.y + this.shadowLength * Math.cos(-angle - Math.PI / 2); + angles.push(angle); + points.push({ + endX, + endY, + startX: dot.x, + startY: dot.y + }); + } + + for (let i = points.length - 1; i >= 0; i--) { + const n = i === 3 ? 0 : i + 1; + ctx.beginPath(); + ctx.moveTo(points[i].startX, points[i].startY); + ctx.lineTo(points[n].startX, points[n].startY); + ctx.lineTo(points[n].endX, points[n].endY); + ctx.lineTo(points[i].endX, points[i].endY); + ctx.fillStyle = this.shadowColor; + ctx.fill(); + } + } + + setSize(size) { + this._initialSize = size; + this.halfSize = Math.floor(size / 2); + } + +} + +/** + * Основано на http://codepen.io/mladen___/pen/gbvqBo + */ +class BoxesField { + + /** + * @param {Node} elem - canvas DOM node + * @param {object} params + */ + constructor(elem, params = { + countBoxes: 14, + boxMinSize: 20, + boxMaxSize: 75, + backgroundColor: '#233d49', + lightColor: '#28555b', + shadowColor: '#274451', + boxColors: ['#207e5c', '#5b9aa9', '#e66c69', '#6b5b8c', '#8b5d79', '#dd8650'] + }) { + this.elem = elem; + this.ctx = elem.getContext('2d'); + this.params = params; + + this.light = { + x: 160, + y: 200 + }; + + this.resize(); + this.drawLoop(); + this.bindWindowListeners(); + + /** + * @type {Box[]} + */ + this.boxes = []; + while (this.boxes.length < this.params.countBoxes) { + this.boxes.push(new Box({ + size: Math.floor((Math.random() * (this.params.boxMaxSize - this.params.boxMinSize)) + this.params.boxMinSize), + startX: Math.floor((Math.random() * elem.width) + 1), + startY: Math.floor((Math.random() * elem.height) + 1), + startRotate: Math.random() * Math.PI, + color: this.getRandomColor(), + shadowColor: this.params.shadowColor + })); + } + } + + resize() { + const { width, height } = this.elem.getBoundingClientRect(); + this.elem.width = width; + this.elem.height = height; + } + + drawLight(light) { + const greaterSize = window.screen.width > window.screen.height ? window.screen.width : window.screen.height; + // еее, теорема пифагора и описывание окружности вокруг квадрата, не зря в универ ходил!!! + const lightRadius = greaterSize * Math.sqrt(2); + + this.ctx.beginPath(); + this.ctx.arc(light.x, light.y, lightRadius, 0, 2 * Math.PI); + const gradient = this.ctx.createRadialGradient(light.x, light.y, 0, light.x, light.y, lightRadius); + gradient.addColorStop(0, this.params.lightColor); + gradient.addColorStop(1, this.params.backgroundColor); + this.ctx.fillStyle = gradient; + this.ctx.fill(); + } + + drawLoop() { + this.ctx.clearRect(0, 0, this.elem.width, this.elem.height); + this.drawLight(this.light); + + for (let i in this.boxes) { + const box = this.boxes[i]; + box.rotate(); + box.drawShadow(this.ctx, this.light); + } + + for (let i in this.boxes) { + const box = this.boxes[i]; + box.draw(this.ctx); + + // Если квадратик вылетел за пределы экрана + if (box.y - box.halfSize > this.elem.height) { + box.y -= this.elem.height + 100; + this.updateBox(box); + } + + if (box.x - box.halfSize > this.elem.width) { + box.x -= this.elem.width + 100; + this.updateBox(box); + } + } + + requestAnimationFrame(this.drawLoop.bind(this)); + } + + bindWindowListeners() { + window.addEventListener('resize', this.resize.bind(this)); + window.addEventListener('mousemove', (event) => { + this.light.x = event.clientX; + this.light.y = event.clientY; + }); + } + + /** + * @param {Box} box + */ + updateBox(box) { + box.color = this.getRandomColor(); + } + + getRandomColor() { + return this.params.boxColors[Math.floor((Math.random() * this.params.boxColors.length))]; + } + +} diff --git a/src/components/ui/bsod/dispatchBsod.js b/src/components/ui/bsod/dispatchBsod.js index 0954035..ef97070 100644 --- a/src/components/ui/bsod/dispatchBsod.js +++ b/src/components/ui/bsod/dispatchBsod.js @@ -12,7 +12,7 @@ export default function dispatchBsod(store = injectedStore) { onBsod && onBsod(); ReactDOM.render( - , + , document.getElementById('app') ); } diff --git a/src/components/ui/bsod/styles.scss b/src/components/ui/bsod/styles.scss new file mode 100644 index 0000000..346733f --- /dev/null +++ b/src/components/ui/bsod/styles.scss @@ -0,0 +1,56 @@ +@import '~components/ui/colors.scss'; + +$font-family-monospaced: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Roboto Mono', monospace; + +.body { + height: 100%; + background-color: $dark_blue; + color: #fff; + text-align: center; + font-family: $font-family-monospaced; + box-sizing: border-box; +} + +.canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.wrapper { + position: relative; + margin: 85px auto 0; + max-width: 500px; + padding: 0 20px; +} + +.title { + font-size: 26px; + margin-bottom: 13px; +} + +.line { + margin: 0 auto; + font-size: 16px; + color: #EBE8E1; +} + +.lineWithMargin { + composes: line; + + margin-bottom: 20px; +} + +.support { + font-size: 18px; + color: #FFF; + margin: 3px 0 44px; + display: block; +} + +.easterEgg { + font-size: 14px; + color: #EBE8E1; +} diff --git a/src/i18n/be.json b/src/i18n/be.json index 7970aad..9e7168d 100644 --- a/src/i18n/be.json +++ b/src/i18n/be.json @@ -133,6 +133,9 @@ "components.profile.preferencesDescription": "Тут вы можаце змяніць асноўныя параметры вашага акаўнта. Звярніце ўвагу, што для ўсіх дзеянняў неабходна пацверджанне з дапамогай уводу пароля.", "components.profile.projectRules": "правілах праекта", "components.profile.twoFactorAuth": "Двухфактарная аўтэнтыфікацыя", + "components.ui.bsod.alsoYouCanInteractWithBackground": "Таксама вы можаце пагуляцца з фонам - ён інтэрактыўны ;)", + "components.ui.bsod.criticalErrorHappened": "Здарылася крытычная памылка, з-за якой сэрвіс не можа працягваць сваю звычайную працу.", + "components.ui.bsod.reloadPageOrContactUs": "Калі ласка, перезагрузіце старонку, каб паўтарыць спробу. Калі праблема ўзнікае зноў, паведаміце аб гэтым распрацоўшчыкам, даслаў пісьмо на адрас", "components.userbar.login": "Уваход", "components.userbar.register": "Рэгістрацыя", "pages.404.homePage": "галоўную старонку", diff --git a/src/i18n/en.json b/src/i18n/en.json index 65d4322..c6f8aa4 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -133,6 +133,9 @@ "components.profile.preferencesDescription": "Here you can change the key preferences of your account. Please note that all actions must be confirmed by entering a password.", "components.profile.projectRules": "project rules", "components.profile.twoFactorAuth": "Two factor auth", + "components.ui.bsod.alsoYouCanInteractWithBackground": "You can also play around with the background – it's interactable ;)", + "components.ui.bsod.criticalErrorHappened": "There was a critical error due to which the application can not continue its normal operation.", + "components.ui.bsod.reloadPageOrContactUs": "Please reload this page and try again. If problem occurs again, please report it to the developers by sending email to", "components.userbar.login": "Sign in", "components.userbar.register": "Join", "pages.404.homePage": "main page", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 6cd3047..79c231c 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -133,6 +133,9 @@ "components.profile.preferencesDescription": "Здесь вы можете сменить ключевые параметры вашего аккаунта. Обратите внимание, что для всех действий необходимо подтверждение при помощи ввода пароля.", "components.profile.projectRules": "правилами проекта", "components.profile.twoFactorAuth": "Двухфакторная аутентификация", + "components.ui.bsod.alsoYouCanInteractWithBackground": "А ещё вы можете потыкать на фон - он интерактивный ;)", + "components.ui.bsod.criticalErrorHappened": "Случилась критическая ошибка, из-за которой приложение не может продолжить свою нормальную работу.", + "components.ui.bsod.reloadPageOrContactUs": "Перезагрузите страницу, чтобы повторить попытку. Если проблема возникает вновь, сообщите об этом разработчикам, отправив письмо на адрес", "components.userbar.login": "Вход", "components.userbar.register": "Регистрация", "pages.404.homePage": "главную страницу", diff --git a/src/i18n/uk.json b/src/i18n/uk.json index eeef6bb..bd6bfc8 100644 --- a/src/i18n/uk.json +++ b/src/i18n/uk.json @@ -133,6 +133,9 @@ "components.profile.preferencesDescription": "Тут ви можете змінити ключові параметри вашого облікового запису. Зверніть увагу, що для всіх дій необхідно підтвердження за допомогою введення пароля.", "components.profile.projectRules": "правилами проекта", "components.profile.twoFactorAuth": "Двофакторна аутентифікація", + "components.ui.bsod.alsoYouCanInteractWithBackground": "А ещё вы можете потыкать на фон - он интерактивный ;)", + "components.ui.bsod.criticalErrorHappened": "Случилась критическая ошибка, из-за которой приложение не может продолжить свою нормальную работу.", + "components.ui.bsod.reloadPageOrContactUs": "Перезагрузите страницу, чтобы повторить попытку. Если проблема возникает вновь, сообщите об этом разработчикам, отправив письмо на адрес", "components.userbar.login": "Вхід", "components.userbar.register": "Реєстрація", "pages.404.homePage": "", diff --git a/src/index.js b/src/index.js index 827f62c..3166b6e 100644 --- a/src/index.js +++ b/src/index.js @@ -6,8 +6,6 @@ import ReactDOM from 'react-dom'; import { Provider as ReduxProvider } from 'react-redux'; import { Router, browserHistory } from 'react-router'; -import webFont from 'webfontloader'; - import { factory as userFactory } from 'components/user/factory'; import { IntlProvider } from 'components/i18n'; import routesFactory from 'routes'; @@ -15,6 +13,7 @@ import storeFactory from 'storeFactory'; import bsodFactory from 'components/ui/bsod/factory'; import loader from 'services/loader'; import logger from 'services/logger'; +import font from 'services/font'; logger.init({ sentryCdn: window.SENTRY_CDN @@ -24,21 +23,9 @@ const store = storeFactory(); bsodFactory(store, stopLoading); -const fontLoadingPromise = new Promise((resolve) => - webFont.load({ - classes: false, - active: resolve, - inactive: resolve, // TODO: may be we should track such cases - timeout: 2000, - custom: { - families: ['Roboto', 'Roboto Condensed'] - } - }) -); - Promise.all([ userFactory(store), - fontLoadingPromise + font.load(['Roboto', 'Roboto Condensed']) ]) .then(() => { ReactDOM.render( diff --git a/src/services/font.js b/src/services/font.js new file mode 100644 index 0000000..deb39b1 --- /dev/null +++ b/src/services/font.js @@ -0,0 +1,32 @@ +import webFont from 'webfontloader'; + +export default { + /** + * @param {array} families + * @param {object} options + * @param {bool} [options.external=false] - whether the font should be loaded from external source (e.g. google) + * + * @return {Promise} + */ + load(families = [], options = {}) { + let config = { + custom: {families} + }; + + if (options.external) { + config = { + google: {families} + }; + } + + return new Promise((resolve) => + webFont.load({ + classes: false, + active: resolve, + inactive: resolve, // TODO: may be we should track such cases + timeout: 2000, + ...config + }) + ); + } +};