mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-11-26 08:42:03 +05:30
Add typings for Box and BoxesField classes, split BSoD view into controller and pure view, add storybook, fix support link styles
This commit is contained in:
parent
32ebba63a1
commit
228bc048af
6
packages/app/components/ui/bsod/BSoD.story.tsx
Normal file
6
packages/app/components/ui/bsod/BSoD.story.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import BSoD from './BSoD';
|
||||
|
||||
storiesOf('UI', module).add('BSoD', () => <BSoD />);
|
@ -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 (
|
||||
<div className={styles.body}>
|
||||
<canvas
|
||||
className={styles.canvas}
|
||||
ref={(el: HTMLCanvasElement | null) => el && new BoxesField(el)}
|
||||
/>
|
||||
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.title}>
|
||||
<Message {...appInfo.appName} />
|
||||
</div>
|
||||
<div className={styles.lineWithMargin}>
|
||||
<Message {...messages.criticalErrorHappened} />
|
||||
</div>
|
||||
<div className={styles.line}>
|
||||
<Message {...messages.reloadPageOrContactUs} />
|
||||
</div>
|
||||
<a href={emailUrl} className={styles.support}>
|
||||
support@ely.by
|
||||
</a>
|
||||
<div className={styles.easterEgg}>
|
||||
<Message {...messages.alsoYouCanInteractWithBackground} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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<Props> = ({ lastEventId }) => {
|
||||
let emailUrl = 'mailto:support@ely.by';
|
||||
|
||||
if (lastEventId) {
|
||||
emailUrl += `?subject=Bug report for #${lastEventId}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<canvas
|
||||
className={styles.canvas}
|
||||
ref={(el: HTMLCanvasElement | null) => el && new BoxesField(el)}
|
||||
/>
|
||||
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.title}>
|
||||
<Message {...appInfo.appName} />
|
||||
</div>
|
||||
<div className={styles.lineWithMargin}>
|
||||
<Message {...messages.criticalErrorHappened} />
|
||||
</div>
|
||||
<div className={styles.line}>
|
||||
<Message {...messages.reloadPageOrContactUs} />
|
||||
</div>
|
||||
<a href={emailUrl} className={styles.support}>
|
||||
support@ely.by
|
||||
</a>
|
||||
<div className={styles.easterEgg}>
|
||||
<Message {...messages.alsoYouCanInteractWithBackground} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BSoD;
|
||||
|
29
packages/app/components/ui/bsod/BSoDContainer.tsx
Normal file
29
packages/app/components/ui/bsod/BSoDContainer.tsx
Normal file
@ -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<string>();
|
||||
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 <BSoD lastEventId={lastEventId} />;
|
||||
};
|
||||
|
||||
export default BSoDContainer;
|
@ -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);
|
||||
}
|
||||
}
|
119
packages/app/components/ui/bsod/Box.ts
Normal file
119
packages/app/components/ui/bsod/Box.ts
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Основано на 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<Box>;
|
||||
|
||||
/**
|
||||
* @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)
|
||||
];
|
4
packages/app/components/ui/bsod/Point.ts
Normal file
4
packages/app/components/ui/bsod/Point.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
export const BSOD = 'BSOD';
|
||||
|
||||
/**
|
||||
* @returns {object}
|
||||
*/
|
||||
export function bsod() {
|
||||
return {
|
||||
type: BSOD,
|
||||
};
|
||||
}
|
9
packages/app/components/ui/bsod/actions.ts
Normal file
9
packages/app/components/ui/bsod/actions.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Action } from 'redux';
|
||||
|
||||
export const BSOD = 'BSOD';
|
||||
|
||||
export function bsod(): Action {
|
||||
return {
|
||||
type: BSOD,
|
||||
};
|
||||
}
|
@ -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<any>;
|
||||
@ -20,7 +20,7 @@ export default function dispatchBsod(
|
||||
|
||||
ReactDOM.render(
|
||||
<ContextProvider store={store} history={history}>
|
||||
<BSoD />
|
||||
<BSoDContainer />
|
||||
</ContextProvider>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user