split app into more components

This commit is contained in:
Artur Tamborski 2021-01-24 00:59:53 +01:00
parent 46a1f7e2bb
commit 577f982e28
17 changed files with 426 additions and 232 deletions

View File

@ -0,0 +1,13 @@
.AnswerList {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
margin-top: 20px;
margin-left: 22px;
p {
font-family: monospace;
font-size: 18px;
margin: 4px 22px 4px 0;
}
}

View File

@ -0,0 +1,16 @@
import './AnswerList.scss';
interface IAnswerListProps {
answers: Array<string>;
}
export default function AnswerList(props: IAnswerListProps): JSX.Element {
const answers = props.answers.map(a => <p key={a}>{a}</p>);
return (
<div className="AnswerList">
{answers}
</div>
);
}

View File

@ -3,7 +3,7 @@
max-height: 100vh;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 1fr 1fr 1/2fr;
grid-template-rows: 1fr;
grid-column-gap: 0;
grid-row-gap: 0;
}
@ -11,58 +11,3 @@
.Answer {
font-size: smaller;
}
.fileUploader {
width: 180px;
height: 40px;
}
.fileContainer {
width: 100%;
height: 100%;
background: #fff;
box-shadow: none;
border-radius: 0;
padding: 0;
display: flex;
text-align: center;
align-items: center;
justify-content: center;
flex-direction: column;
margin: 0;
p {
display: none;
}
button {
margin: 10px 0;
width: 100%;
height: 100%;
}
}
.UploadSection {
display: flex;
justify-content: center;
align-items: center;
}
.ConfirmButton {
width: 180px;
height: 30px;
padding: 6px 23px;
background: #3f4257;
border-radius: 30px;
color: white;
font-weight: 300;
font-size: 14px;
margin: 10px 10px;
transition: all 0.2s ease-in;
cursor: pointer;
outline: none;
border: none;
}

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -1,22 +1,14 @@
import React from 'react';
import ImageUploader from 'react-images-upload';
import MultiCrops from 'react-multi-crops'
// @ts-ignore
import Logo from '../Logo/Logo';
// @ts-ignore
import AnswerList from "../AnswerList/AnswerList";
import Board from '../Board/Board';
import Logo from "../Logo/Logo";
import Menu from '../Menu/Menu';
import Randomizer from "../Randomizer/Randomizer";
import Recognizer from "../Recognizer/Recognizer";
import Title from "../Title/Title";
import './App.scss';
// @ts-ignore
import {findTextRegions} from "../../helpers/findTextRegions";
// @ts-ignore
import {
recognizeTextOnImage,
// @ts-ignore
recognizeTextOnImageGrid
} from "../../helpers/recognizeTextOnImage";
export type Point = {
x: number;
@ -33,194 +25,127 @@ export type Solution = {
key: string;
}
type Game = {
export type Game = {
cells: Array<Array<string>>;
solutions: Array<Solution>;
title: string;
description: string;
catchword: string;
cells: Array<Array<string>>;
solutions: Array<Solution>;
}
interface IAppProps {
}
interface IAppState {
image: HTMLImageElement | null,
selections: Array<any>;
cells: Array<Array<string>>;
solutions: Array<Solution>;
export interface IAppState {
game: Game;
mode: string;
ready: boolean;
}
export default class App extends React.Component<IAppProps, IAppState> {
private game: Game;
constructor(props: IAppProps) {
super(props);
const gameId = (Math.trunc(Math.random() * 100) % 2) + 1;
this.game = require(`../../constants/${gameId}.json`);
this.state = {
image: null,
selections: [],
cells: [],
solutions: [],
game: {
cells: [],
solutions: [],
title: "",
description: "",
catchword: "",
},
mode: "",
ready: false,
}
}
handleTakePhoto(pictures: any[], _: any[]): void {
const image = document.createElement('img');
image.src = URL.createObjectURL(pictures[0]);
image.onload = () => this.setState({...this.state, image});
handleGameModeSelected(mode: string) {
const ready = mode === "reset" ? false : this.state.ready;
this.setState({...this.state, mode, ready});
}
handleChangeCoordinate(_: any, __: any, selections: any) {
this.setState({...this.state, selections});
handleRecognitionFinished(game: Game) {
this.setState({...this.state, game, ready: true});
}
handleConfirmClick() {
if (!this.state.image || !this.state.selections.length)
return;
renderTitle(): JSX.Element {
if (!this.state.ready) return <div />;
const mainCanvas = document.createElement('canvas');
const mainContext = mainCanvas.getContext('2d');
mainCanvas.width = this.state.image.width;
mainCanvas.height = this.state.image.height;
mainContext?.drawImage(this.state.image, 0, 0);
const areas = this.state.selections.map(s => s.width * s.height);
const gridIndex = areas.indexOf(Math.max(...areas));
const gridSelection = this.state.selections.splice(gridIndex, 1)[0];
const gridImage = mainContext?.getImageData(
gridSelection.x,
gridSelection.y,
gridSelection.width,
gridSelection.height
return (
<Title
title={this.state.game.title}
description={this.state.game.description}
/>
);
if (!gridImage) {
console.log("gridImage is empty");
return;
}
const gridTextRegions = findTextRegions(gridImage);
if (!gridTextRegions || !gridTextRegions.grid) {
console.log("gridRegions is empty");
return;
}
let reads = [];
reads.push(this.handleGridTextRegionsReadyToRead(gridTextRegions.grid));
for (let s of this.state.selections) {
const tempData = mainContext?.getImageData(s.x, s.y, s.width, s.height);
if (!tempData) {
console.log("tempData is empty");
continue;
}
reads.push(this.handleStateSelectionsReadyToRead(tempData, s))
}
Promise.all(reads).then(() => {
// free resources
if (this.state.image) {
URL.revokeObjectURL(this.state.image.src);
mainCanvas.width = 0;
mainCanvas.height = 0;
}
this.setState({
...this.state,
image: null,
selections: [],
});
});
}
async handleGridTextRegionsReadyToRead(grid: any) {
const textGrid = await recognizeTextOnImageGrid(grid);
let cells = [];
for (let row of textGrid) {
let line = [];
for (let letter of row) {
line.push(letter.text);
}
cells.push(line);
}
this.setState({...this.state, cells});
renderLogo(): JSX.Element {
return <Logo />;
}
async handleStateSelectionsReadyToRead(data: ImageData, s: any) {
const tempCanvas = document.createElement('canvas');
const tempContext = tempCanvas.getContext('2d');
tempCanvas.width = s.width;
tempCanvas.height = s.height;
tempContext?.putImageData(data, 0, 0);
const text = await recognizeTextOnImage(tempCanvas);
let solutions = this.state.solutions.slice();
for (let key of text.split("\n")) {
solutions.push({key, selection: {start: {x: 0, y: 0}, end: {x: 0, y: 0}}});
}
this.setState({...this.state, solutions});
renderMenu(): JSX.Element {
return (
<Menu
onGameModeSelected={this.handleGameModeSelected.bind(this)}
/>
);
}
renderAnswers(): Array<JSX.Element> {
return this.game.solutions.map(s => s.key).map(k =>
<p key={k} className="Answer">{k}</p>);
renderBoard(): JSX.Element {
if (!this.state.ready) return <div />;
return (
<Board
cells={this.state.game.cells}
solutions={this.state.game.solutions}
cellSize={60}
/>
);
}
renderRandomizer(): JSX.Element {
if (!(!this.state.ready && this.state.mode === "randomize")) return <div />;
return (
<Randomizer
onRecognitionFinished={this.handleRecognitionFinished.bind(this)}
/>
);
}
renderRecognizer(): JSX.Element {
if (!(!this.state.ready && this.state.mode === "recognize")) return <div />;
return (
<Recognizer
onRecognitionFinished={this.handleRecognitionFinished.bind(this)}
/>
);
}
renderAnswerList(): JSX.Element {
if (!this.state.ready) return <></>;
const answers = this.state.game.solutions.map(s => s.key);
return (
<AnswerList
answers={answers}
/>
);
}
render() {
return (
<div className="App">
<div />
<div>
<Logo />
<div>
<p>{this.game.title}</p>
<p>{this.game.description}</p>
</div>
</div>
<div />
<div style={{paddingLeft: '300px'}}>
<div className="UploadSection">
<ImageUploader
withIcon={false}
buttonText='Wrzuć zdjęcie!'
onChange={this.handleTakePhoto.bind(this)}
imgExtension={['.jpg', '.jpeg', '.png']}
maxFileSize={5242880 * 5} // 5MB * 5
/>
<button
className="ConfirmButton"
onClick={this.handleConfirmClick.bind(this)}
style={{visibility: this.state.image ? 'visible' : 'hidden'}}
>Potwierdź</button>
</div>
<MultiCrops
src={this.state.image?.src || ''}
width={this.state.image?.width}
coordinates={this.state.selections}
onChange={this.handleChangeCoordinate.bind(this)}
/>
</div>
<div>
<Board
cells={this.game.cells}
solutions={this.game.solutions}
cellSize={60}
/>
</div>
<div style={{paddingLeft: '30px'}}>
{this.renderAnswers()}
</div>
<div /> {this.renderLogo()} <div />
<div /> {this.renderMenu()} <div />
<div /> {this.renderTitle()} <div />
<div /> {this.renderBoard()} <div />
<div /> {this.renderRecognizer()} <div />
<div /> {this.renderRandomizer()} <div />
<div /> {this.renderAnswerList()} <div />
</div>
);
}

View File

@ -127,7 +127,9 @@ export default class Board extends React.Component<IBoardProps, IBoardState> {
let lines = this.state.answers.map((s, n) =>
this.renderLine(s, n));
const gridTemplateColumns = `repeat(${this.props.cells[0].length}, ${this.props.cellSize}px)`;
const x = this.props.cells[0].length;
const y = this.props.cellSize;
const gridTemplateColumns = `repeat(${x}, ${y}px)`;
return (
<div className="Container">

View File

@ -21,6 +21,7 @@ interface ILineProps {
}
export default function Line(props: ILineProps): JSX.Element {
const [m1, sqrt2] = [props.cellSize, Math.sqrt(2)];
const [m2, m4, m8] = [m1 / 2, m1 / 4, m1 / 8];
const [startX, endX] = [m1 * props.selection.start.x, m1 * props.selection.end.x];

View File

@ -1,4 +1,5 @@
.Logo {
width: 100%;
display: flex;
justify-content: center;
padding: 20px 0;

View File

@ -2,6 +2,7 @@ import Wide from './crosski_wide.svg'
import './Logo.scss';
export default function Logo(): JSX.Element {
return (
<div className="Logo">
<img alt="Logo" src={Wide}/>

View File

@ -0,0 +1,9 @@
.Menu {
width: 100%;
display: flex;
justify-content: space-evenly;
button {
font-size: 1.3rem;
}
}

View File

@ -0,0 +1,20 @@
import './Menu.scss';
interface IMenuProps {
onGameModeSelected: (mode: string) => void;
}
export default function Menu(props: IMenuProps): JSX.Element {
const randomize = () => props.onGameModeSelected('randomize');
const recognize = () => props.onGameModeSelected('recognize');
const reset = () => props.onGameModeSelected('reset');
return (
<div className="Menu">
<button onClick={randomize}>randomize</button>
<button onClick={reset}>reset</button>
<button onClick={recognize}>recognize</button>
</div>
);
}

View File

@ -0,0 +1,14 @@
import {Game} from "../App/App";
interface IRandomizerProps {
onRecognitionFinished: (game: Game) => void;
}
export default function Randomizer(props: IRandomizerProps): JSX.Element {
const gameId = (Math.trunc(Math.random() * 100) % 2) + 1;
const game = require(`../../constants/${gameId}.json`) as Game;
props.onRecognitionFinished(game);
return <div />;
}

View File

@ -0,0 +1,55 @@
.Recognizer {
display: flex;
padding-top: 20px;
flex-flow: column;
justify-content: center;
align-items: center;
}
.fileUploader {
width: 180px;
height: 40px;
}
.fileContainer {
width: 100%;
height: 100%;
background: #fff;
box-shadow: none;
border-radius: 0;
padding: 0;
display: flex;
text-align: center;
align-items: center;
justify-content: center;
flex-direction: column;
margin: 0;
p {
display: none;
}
button {
margin: 10px 0;
width: 100%;
height: 100%;
}
}
.ConfirmButton {
width: 180px;
height: 30px;
padding: 6px 23px;
background: #3f4257;
border-radius: 30px;
color: white;
font-weight: 300;
font-size: 14px;
margin: 10px 10px;
transition: all 0.2s ease-in;
cursor: pointer;
outline: none;
border: none;
}

View File

@ -0,0 +1,173 @@
import React from "react";
import ImageUploader from "react-images-upload";
import MultiCrops from "react-multi-crops";
import {findTextRegions} from "../../helpers/findTextRegions";
import {
recognizeTextOnImage,
recognizeTextOnImageGrid
} from "../../helpers/recognizeTextOnImage";
import {Game, Solution} from "../App/App";
import "./Recognizer.scss";
interface IRecognizerProps {
onRecognitionFinished: (game: Game) => void;
}
interface IRecognizerState {
image: HTMLImageElement | null,
cells: Array<Array<string>>;
solutions: Array<Solution>;
coordinates: Array<any>;
}
export default class Recognizer extends React.Component<IRecognizerProps, IRecognizerState> {
constructor(props: IRecognizerProps) {
super(props);
this.state = {
image: null,
cells: [],
solutions: [],
coordinates: [],
}
}
handleTakePhoto(files: any[], _: any[]): void {
const image = document.createElement('img');
image.src = URL.createObjectURL(files[0]);
image.onload = () => this.setState({...this.state, image});
}
handleChangeCoordinate(_: any, __: any, selections: any) {
this.setState({...this.state, coordinates: selections});
}
handleConfirmClick() {
if (!this.state.image || !this.state.coordinates.length)
return;
const mainCanvas = document.createElement('canvas');
const mainContext = mainCanvas.getContext('2d');
mainCanvas.width = this.state.image.width;
mainCanvas.height = this.state.image.height;
mainContext?.drawImage(this.state.image, 0, 0);
const areas = this.state.coordinates.map(s => s.width * s.height);
const gridIndex = areas.indexOf(Math.max(...areas));
const gridSelection = this.state.coordinates.splice(gridIndex, 1)[0];
const gridImage = mainContext?.getImageData(
gridSelection.x,
gridSelection.y,
gridSelection.width,
gridSelection.height
);
if (!gridImage) {
console.log("gridImage is empty");
return;
}
const gridTextRegions = findTextRegions(gridImage);
if (!gridTextRegions || !gridTextRegions.grid) {
console.log("gridRegions is empty");
return;
}
let reads = [];
reads.push(this.handleGridTextRegionsReadyToRead(gridTextRegions.grid));
for (let s of this.state.coordinates) {
const tempData = mainContext?.getImageData(s.x, s.y, s.width, s.height);
if (!tempData) {
console.log("tempData is empty");
continue;
}
reads.push(this.handleStateSelectionsReadyToRead(tempData, s))
}
Promise.all(reads).then(() => {
// free resources
if (this.state.image) {
URL.revokeObjectURL(this.state.image.src);
mainCanvas.width = 0;
mainCanvas.height = 0;
}
this.setState({
...this.state,
image: null,
coordinates: [],
});
this.props.onRecognitionFinished({
...this.state,
title: "Loaded game",
description: "Good luck!",
catchword: "",
});
});
}
async handleGridTextRegionsReadyToRead(grid: any) {
const textGrid = await recognizeTextOnImageGrid(grid);
let cells = [];
for (let row of textGrid) {
let line = [];
for (let letter of row) {
line.push(letter.text);
}
cells.push(line);
}
this.setState({...this.state, cells});
}
async handleStateSelectionsReadyToRead(data: ImageData, s: any) {
const tempCanvas = document.createElement('canvas');
const tempContext = tempCanvas.getContext('2d');
tempCanvas.width = s.width;
tempCanvas.height = s.height;
tempContext?.putImageData(data, 0, 0);
const text = await recognizeTextOnImage(tempCanvas);
let solutions = this.state.solutions.slice();
for (let key of text.split("\n")) {
solutions.push({key, selection: {start: {x: 0, y: 0}, end: {x: 0, y: 0}}});
}
this.setState({...this.state, solutions});
}
render() {
return (
<div className="Recognizer">
<ImageUploader
withIcon={false}
buttonText='Wybierz zdjęcie'
onChange={this.handleTakePhoto.bind(this)}
imgExtension={['.jpg', '.jpeg', '.png']}
maxFileSize={5242880 * 5} // 5MB * 5
/>
<button
className="ConfirmButton"
onClick={this.handleConfirmClick.bind(this)}
style={{visibility: this.state.image ? 'visible' : 'hidden'}}
>Potwierdź</button>
<MultiCrops
src={this.state.image?.src || ''}
width={this.state.image?.width}
coordinates={this.state.coordinates}
onChange={this.handleChangeCoordinate.bind(this)}
/>
</div>
);
}
}

View File

@ -0,0 +1,8 @@
.Title {
text-align: center;
margin: 15px 80px;
p {
margin: 4px;
}
}

View File

@ -0,0 +1,16 @@
import './Title.scss';
interface ITitleProps {
title: string;
description: string;
}
export default function Title(props: ITitleProps): JSX.Element {
return (
<div className="Title">
<p>{props.title}</p>
<span>{props.description}</span>
</div>
);
}

View File

@ -11,4 +11,8 @@ html, body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #FFEFBA; /* fallback for old browsers */
background: -webkit-linear-gradient(to right, #FFFFFF, #FFEFBA); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to right, #FFFFFF, #FFEFBA); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}