From 91ac512507ed8a6e8856c262aa7d5cb88833ce2b Mon Sep 17 00:00:00 2001 From: Artur Tamborski Date: Sun, 24 Jan 2021 00:59:53 +0100 Subject: [PATCH] split app into more components --- src/components/AnswerList/AnswerList.scss | 13 ++ src/components/AnswerList/AnswerList.tsx | 16 ++ src/components/App/App.scss | 57 +---- src/components/App/App.test.tsx | 9 - src/components/App/App.tsx | 257 ++++++++-------------- src/components/Board/Board.tsx | 4 +- src/components/Line/Line.tsx | 1 + src/components/Logo/Logo.scss | 1 + src/components/Logo/Logo.tsx | 1 + src/components/Menu/Menu.scss | 9 + src/components/Menu/Menu.tsx | 20 ++ src/components/Randomizer/Randomizer.tsx | 14 ++ src/components/Recognizer/Recognizer.scss | 55 +++++ src/components/Recognizer/Recognizer.tsx | 173 +++++++++++++++ src/components/Title/Title.scss | 8 + src/components/Title/Title.tsx | 16 ++ src/index.css | 4 + 17 files changed, 426 insertions(+), 232 deletions(-) create mode 100644 src/components/AnswerList/AnswerList.scss create mode 100644 src/components/AnswerList/AnswerList.tsx delete mode 100644 src/components/App/App.test.tsx create mode 100644 src/components/Menu/Menu.scss create mode 100644 src/components/Menu/Menu.tsx create mode 100644 src/components/Randomizer/Randomizer.tsx create mode 100644 src/components/Recognizer/Recognizer.scss create mode 100644 src/components/Recognizer/Recognizer.tsx create mode 100644 src/components/Title/Title.scss create mode 100644 src/components/Title/Title.tsx diff --git a/src/components/AnswerList/AnswerList.scss b/src/components/AnswerList/AnswerList.scss new file mode 100644 index 0000000..d22b911 --- /dev/null +++ b/src/components/AnswerList/AnswerList.scss @@ -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; + } +} diff --git a/src/components/AnswerList/AnswerList.tsx b/src/components/AnswerList/AnswerList.tsx new file mode 100644 index 0000000..8809970 --- /dev/null +++ b/src/components/AnswerList/AnswerList.tsx @@ -0,0 +1,16 @@ +import './AnswerList.scss'; + +interface IAnswerListProps { + answers: Array; +} + +export default function AnswerList(props: IAnswerListProps): JSX.Element { + + const answers = props.answers.map(a =>

{a}

); + +return ( +
+ {answers} +
+ ); +} diff --git a/src/components/App/App.scss b/src/components/App/App.scss index 747d054..bddaa79 100644 --- a/src/components/App/App.scss +++ b/src/components/App/App.scss @@ -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; -} diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx deleted file mode 100644 index 2a68616..0000000 --- a/src/components/App/App.test.tsx +++ /dev/null @@ -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(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 93871e3..f64a4a7 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -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>; + solutions: Array; title: string; description: string; catchword: string; - cells: Array>; - solutions: Array; } interface IAppProps { } -interface IAppState { - image: HTMLImageElement | null, - selections: Array; - cells: Array>; - solutions: Array; +export interface IAppState { + game: Game; + mode: string; + ready: boolean; } export default class App extends React.Component { - 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
; - 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 ( + ); - - 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> ); } diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 9116cfa..f82042e 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -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"> diff --git a/src/components/Line/Line.tsx b/src/components/Line/Line.tsx index 375a0d7..76584ca 100644 --- a/src/components/Line/Line.tsx +++ b/src/components/Line/Line.tsx @@ -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]; diff --git a/src/components/Logo/Logo.scss b/src/components/Logo/Logo.scss index ae49675..f56305e 100644 --- a/src/components/Logo/Logo.scss +++ b/src/components/Logo/Logo.scss @@ -1,4 +1,5 @@ .Logo { + width: 100%; display: flex; justify-content: center; padding: 20px 0; diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index 5f9a62d..d461f8f 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -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}/> diff --git a/src/components/Menu/Menu.scss b/src/components/Menu/Menu.scss new file mode 100644 index 0000000..8d35f43 --- /dev/null +++ b/src/components/Menu/Menu.scss @@ -0,0 +1,9 @@ +.Menu { + width: 100%; + display: flex; + justify-content: space-evenly; + + button { + font-size: 1.3rem; + } +} diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx new file mode 100644 index 0000000..d95019b --- /dev/null +++ b/src/components/Menu/Menu.tsx @@ -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> + ); +} diff --git a/src/components/Randomizer/Randomizer.tsx b/src/components/Randomizer/Randomizer.tsx new file mode 100644 index 0000000..6c58e9d --- /dev/null +++ b/src/components/Randomizer/Randomizer.tsx @@ -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 />; +} diff --git a/src/components/Recognizer/Recognizer.scss b/src/components/Recognizer/Recognizer.scss new file mode 100644 index 0000000..e7d23e7 --- /dev/null +++ b/src/components/Recognizer/Recognizer.scss @@ -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; +} diff --git a/src/components/Recognizer/Recognizer.tsx b/src/components/Recognizer/Recognizer.tsx new file mode 100644 index 0000000..52dffcd --- /dev/null +++ b/src/components/Recognizer/Recognizer.tsx @@ -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> + ); + } +} diff --git a/src/components/Title/Title.scss b/src/components/Title/Title.scss new file mode 100644 index 0000000..bdce4c2 --- /dev/null +++ b/src/components/Title/Title.scss @@ -0,0 +1,8 @@ +.Title { + text-align: center; + margin: 15px 80px; + + p { + margin: 4px; + } +} diff --git a/src/components/Title/Title.tsx b/src/components/Title/Title.tsx new file mode 100644 index 0000000..d6379c2 --- /dev/null +++ b/src/components/Title/Title.tsx @@ -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> + ); +} diff --git a/src/index.css b/src/index.css index 6021fcc..d1a1481 100644 --- a/src/index.css +++ b/src/index.css @@ -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+ */ }