diff --git a/packages/app/components/ui/bsod/BSoD.story.tsx b/packages/app/components/ui/bsod/BSoD.story.tsx new file mode 100644 index 0000000..332c1fa --- /dev/null +++ b/packages/app/components/ui/bsod/BSoD.story.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import BSoD from './BSoD'; + +storiesOf('UI', module).add('BSoD', () => ); diff --git a/packages/app/components/ui/bsod/BSoD.tsx b/packages/app/components/ui/bsod/BSoD.tsx index 3cd2397..bd3bdca 100644 --- a/packages/app/components/ui/bsod/BSoD.tsx +++ b/packages/app/components/ui/bsod/BSoD.tsx @@ -1,72 +1,56 @@ -import React from 'react'; +import React, { ComponentType } from 'react'; import { FormattedMessage as Message } from 'react-intl'; -import logger from 'app/services/logger'; + import appInfo from 'app/components/auth/appInfo/AppInfo.intl.json'; -import styles from './styles.scss'; import BoxesField from './BoxesField'; + +import styles from './styles.scss'; import messages from './BSoD.intl.json'; interface State { lastEventId?: string | void; } -// TODO: probably it is better to render this view from the App view -// to remove dependencies from store and IntlProvider -class BSoD extends React.Component<{}, State> { - state: State = {}; - - componentDidMount() { - // poll for event id - const timer = setInterval(() => { - if (!logger.getLastEventId()) { - return; - } - - clearInterval(timer); - - this.setState({ - lastEventId: logger.getLastEventId(), - }); - }, 500); - } - - render() { - const { lastEventId } = this.state; - - let emailUrl = 'mailto:support@ely.by'; - - if (lastEventId) { - emailUrl += `?subject=Bug report for #${lastEventId}`; - } - - return ( -
- el && new BoxesField(el)} - /> - -
-
- -
-
- -
-
- -
- - support@ely.by - -
- -
-
-
- ); - } +interface Props { + lastEventId?: string; } +// TODO: probably it's better to render this view from the App view +// to remove dependencies from the store and IntlProvider +const BSoD: ComponentType = ({ lastEventId }) => { + let emailUrl = 'mailto:support@ely.by'; + + if (lastEventId) { + emailUrl += `?subject=Bug report for #${lastEventId}`; + } + + return ( +
+ el && new BoxesField(el)} + /> + +
+
+ +
+
+ +
+
+ +
+ + support@ely.by + +
+ +
+
+
+ ); +}; + export default BSoD; diff --git a/packages/app/components/ui/bsod/BSoDContainer.tsx b/packages/app/components/ui/bsod/BSoDContainer.tsx new file mode 100644 index 0000000..422b895 --- /dev/null +++ b/packages/app/components/ui/bsod/BSoDContainer.tsx @@ -0,0 +1,29 @@ +import React, { ComponentType, useEffect, useState } from 'react'; + +import logger from 'app/services/logger/logger'; + +import BSoD from './BSoD'; + +const BSoDContainer: ComponentType = () => { + const [lastEventId, setLastEventId] = useState(); + useEffect(() => { + const timer = setInterval(() => { + // eslint-disable-next-line no-shadow + const lastEventId = logger.getLastEventId(); + + if (!lastEventId) { + return; + } + + clearInterval(timer); + setLastEventId(lastEventId); + }, 500); + + // Don't care about interval cleanup because there is no way from + // BSoD state and page can be only reloaded + }, []); + + return ; +}; + +export default BSoDContainer; diff --git a/packages/app/components/ui/bsod/Box.js b/packages/app/components/ui/bsod/Box.js deleted file mode 100644 index ed04b6d..0000000 --- a/packages/app/components/ui/bsod/Box.js +++ /dev/null @@ -1,100 +0,0 @@ -export default class Box { - constructor({ size, startX, startY, startRotate, color, shadowColor }) { - this.color = color; - this.shadowColor = shadowColor; - this.halfSize = 0; - 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; - 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; - const angles = []; - const points = []; - - for (const i in dots) { - if (!dots.hasOwnProperty(i)) { - continue; - } - - 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); - } -} diff --git a/packages/app/components/ui/bsod/Box.ts b/packages/app/components/ui/bsod/Box.ts new file mode 100644 index 0000000..5fa9726 --- /dev/null +++ b/packages/app/components/ui/bsod/Box.ts @@ -0,0 +1,119 @@ +import Point from './Point'; + +const shadowLength = 2000; // TODO: should be calculated + +export default class Box { + public position: Point; + private angle: number; + public color: string; + private readonly shadowColor: string; + + private _size: number; + private _halfSize: number; + + constructor( + size: number, + position: Point, + startRotate: number, + color: string, + shadowColor: string, + ) { + this.size = size; + this.position = position; + this.color = color; + this.angle = startRotate; + this.shadowColor = shadowColor; + } + + public get size(): number { + return this._size; + } + + public set size(size: number) { + this._size = size; + this._halfSize = Math.floor(size / 2); + } + + public get halfSize(): number { + return this._halfSize; + } + + get points(): { p1: Point; p2: Point; p3: Point; p4: Point } { + const full = (Math.PI * 2) / 4; + + const p1: Point = { + x: this.position.x + this._halfSize * Math.sin(this.angle), + y: this.position.y + this._halfSize * Math.cos(this.angle), + }; + + const p2: Point = { + x: this.position.x + this._halfSize * Math.sin(this.angle + full), + y: this.position.y + this._halfSize * Math.cos(this.angle + full), + }; + + const p3: Point = { + x: this.position.x + this._halfSize * Math.sin(this.angle + full * 2), + y: this.position.y + this._halfSize * Math.cos(this.angle + full * 2), + }; + + const p4: Point = { + x: this.position.x + this._halfSize * Math.sin(this.angle + full * 3), + y: this.position.y + this._halfSize * Math.cos(this.angle + full * 3), + }; + + return { p1, p2, p3, p4 }; + } + + rotate(): void { + const speed = (60 - this._halfSize) / 20; + this.angle += speed * 0.002; + this.position.x += speed; + this.position.y += speed; + } + + draw(ctx: CanvasRenderingContext2D): void { + const { points } = this; + ctx.beginPath(); + ctx.moveTo(points.p1.x, points.p1.y); + ctx.lineTo(points.p2.x, points.p2.y); + ctx.lineTo(points.p3.x, points.p3.y); + ctx.lineTo(points.p4.x, points.p4.y); + ctx.fillStyle = this.color; + ctx.fill(); + } + + drawShadow(ctx: CanvasRenderingContext2D, light: Point): void { + const boxPoints = this.points; + const points: Array<{ + startX: number; + startY: number; + endX: number; + endY: number; + }> = []; + + // eslint-disable-next-line guard-for-in + for (const i in boxPoints) { + const point = boxPoints[i]; + const angle = Math.atan2(light.y - point.y, light.x - point.x); + const endX = point.x + shadowLength * Math.sin(-angle - Math.PI / 2); + const endY = point.y + shadowLength * Math.cos(-angle - Math.PI / 2); + points.push({ + startX: point.x, + startY: point.y, + endX, + endY, + }); + } + + 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(); + } + } +} diff --git a/packages/app/components/ui/bsod/BoxesField.js b/packages/app/components/ui/bsod/BoxesField.ts similarity index 67% rename from packages/app/components/ui/bsod/BoxesField.js rename to packages/app/components/ui/bsod/BoxesField.ts index 7c279ca..c2870b3 100644 --- a/packages/app/components/ui/bsod/BoxesField.js +++ b/packages/app/components/ui/bsod/BoxesField.ts @@ -1,16 +1,34 @@ +import Point from './Point'; import Box from './Box'; +interface Params { + countBoxes: number; + boxMinSize: number; + boxMaxSize: number; + backgroundColor: string; + lightColor: string; + shadowColor: string; + boxColors: ReadonlyArray; +} + /** - * Основано на http://codepen.io/mladen___/pen/gbvqBo + * Based on http://codepen.io/mladen___/pen/gbvqBo */ export default class BoxesField { + private readonly elem: HTMLCanvasElement; + private readonly ctx: CanvasRenderingContext2D; + private readonly params: Params; + + private light: Point; + private boxes: Array; + /** * @param {HTMLCanvasElement} elem - canvas DOM node * @param {object} params */ constructor( - elem, - params = { + elem: HTMLCanvasElement, + params: Params = { countBoxes: 14, boxMinSize: 20, boxMaxSize: 75, @@ -31,7 +49,7 @@ export default class BoxesField { const ctx = elem.getContext('2d'); if (!ctx) { - throw new Error('Can not get canvas 2d context'); + throw new Error('Cannot get canvas 2d context'); } this.ctx = ctx; @@ -46,35 +64,34 @@ export default class BoxesField { this.drawLoop(); this.bindWindowListeners(); - /** - * @type {Box[]} - */ this.boxes = []; while (this.boxes.length < this.params.countBoxes) { this.boxes.push( - new Box({ - size: Math.floor( + new Box( + 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, - }), + { + x: Math.floor(Math.random() * elem.width + 1), + y: Math.floor(Math.random() * elem.height + 1), + }, + Math.random() * Math.PI, + this.getRandomColor(), + this.params.shadowColor, + ), ); } } - resize() { + resize(): void { const { width, height } = this.elem.getBoundingClientRect(); this.elem.width = width; this.elem.height = height; } - drawLight(light) { + drawLight(light: Point): void { const greaterSize = window.screen.width > window.screen.height ? window.screen.width @@ -98,7 +115,7 @@ export default class BoxesField { this.ctx.fill(); } - drawLoop() { + drawLoop(): void { this.ctx.clearRect(0, 0, this.elem.width, this.elem.height); this.drawLight(this.light); @@ -120,14 +137,20 @@ export default class BoxesField { 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); + // When box leaves window boundaries + let shouldUpdateBox = false; + + if (box.position.y - box.halfSize > this.elem.height) { + box.position.y -= this.elem.height + 100; + shouldUpdateBox = true; } - if (box.x - box.halfSize > this.elem.width) { - box.x -= this.elem.width + 100; + if (box.position.x - box.halfSize > this.elem.width) { + box.position.x -= this.elem.width + 100; + shouldUpdateBox = true; + } + + if (shouldUpdateBox) { this.updateBox(box); } } @@ -143,14 +166,11 @@ export default class BoxesField { }); } - /** - * @param {Box} box - */ - updateBox(box) { + updateBox(box: Box): void { box.color = this.getRandomColor(); } - getRandomColor() { + getRandomColor(): string { return this.params.boxColors[ Math.floor(Math.random() * this.params.boxColors.length) ]; diff --git a/packages/app/components/ui/bsod/Point.ts b/packages/app/components/ui/bsod/Point.ts new file mode 100644 index 0000000..6ebe393 --- /dev/null +++ b/packages/app/components/ui/bsod/Point.ts @@ -0,0 +1,4 @@ +export default interface Point { + x: number; + y: number; +} diff --git a/packages/app/components/ui/bsod/actions.js b/packages/app/components/ui/bsod/actions.js deleted file mode 100644 index f4010f9..0000000 --- a/packages/app/components/ui/bsod/actions.js +++ /dev/null @@ -1,10 +0,0 @@ -export const BSOD = 'BSOD'; - -/** - * @returns {object} - */ -export function bsod() { - return { - type: BSOD, - }; -} diff --git a/packages/app/components/ui/bsod/actions.ts b/packages/app/components/ui/bsod/actions.ts new file mode 100644 index 0000000..2128074 --- /dev/null +++ b/packages/app/components/ui/bsod/actions.ts @@ -0,0 +1,9 @@ +import { Action } from 'redux'; + +export const BSOD = 'BSOD'; + +export function bsod(): Action { + return { + type: BSOD, + }; +} diff --git a/packages/app/components/ui/bsod/dispatchBsod.tsx b/packages/app/components/ui/bsod/dispatchBsod.tsx index e76b293..0cb12c1 100644 --- a/packages/app/components/ui/bsod/dispatchBsod.tsx +++ b/packages/app/components/ui/bsod/dispatchBsod.tsx @@ -5,7 +5,7 @@ import { Store } from 'app/reducers'; import { History } from 'history'; import { bsod } from './actions'; -import BSoD from './BSoD'; +import BSoDContainer from './BSoDContainer'; let injectedStore: Store; let injectedHistory: History; @@ -20,7 +20,7 @@ export default function dispatchBsod( ReactDOM.render( - + , document.getElementById('app'), ); diff --git a/packages/app/components/ui/bsod/styles.scss b/packages/app/components/ui/bsod/styles.scss index ca38ca0..d9ff3fb 100644 --- a/packages/app/components/ui/bsod/styles.scss +++ b/packages/app/components/ui/bsod/styles.scss @@ -1,7 +1,6 @@ @import '~app/components/ui/colors.scss'; -$font-family-monospaced: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Roboto Mono', - monospace; +$font-family-monospaced: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Roboto Mono', monospace; .body { height: 100%; @@ -46,9 +45,16 @@ $font-family-monospaced: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Roboto Mono', .support { font-size: 18px; + line-height: 18px; color: #fff; - margin: 3px 0 44px; - display: block; + margin: 5px 0 44px; + display: inline-block; + border-bottom-color: #39777f; + + &:hover { + color: #fff; + border-bottom-color: #eee; + } } .easterEgg {