diff --git a/.storybook/config.js b/.storybook/config.js index 3c1c5e0..46aa5c3 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -15,6 +15,9 @@ if (process.env.STORYBOOK_FOLDER === 'activities') { if (process.env.STORYBOOK_FOLDER === 'core') { req = require.context('../core', true, /\.stories\.jsx$/); } +if (process.env.STORYBOOK_FOLDER === 'client') { + req = require.context('../client', true, /\.stories\.jsx$/); +} function loadStories() { req.keys().forEach(req); diff --git a/client/CHANGELOG.md b/client/CHANGELOG.md index 4594601..0550b4e 100644 --- a/client/CHANGELOG.md +++ b/client/CHANGELOG.md @@ -1,8 +1,9 @@ ## HEAD -## v1.0.0-alpha.0 +- ⚙️ [v1.1.0] `pages/MenuBar`: Make the menu more simple to reflect the new way to access to exercises (#59). +- ⚙️ [v1.1.0] `pages/links`: Add the exercise name to the link's title when playing (#56). -### 💅 Components +## v1.0.0-alpha.0 - 🆕 [v1.0.0] `FrontPage`. - 🆕 [v1.0.0] `pages/MenuBar`. diff --git a/client/components/pages/MenuBar.jsx b/client/components/pages/MenuBar.jsx index 5e90e37..51e3cdc 100644 --- a/client/components/pages/MenuBar.jsx +++ b/client/components/pages/MenuBar.jsx @@ -51,6 +51,13 @@ const ExternalLinksContainer = styled.div` type Props = {| routes: Array, |}; +/** + * Códimo client menu bar. + * + * @version 1.1.0 + * @param {Array} routes List of routes. + * @return {React$Element} Menu bar element. + */ const MenuBar = ({ routes }: Props) => ( @@ -58,37 +65,19 @@ const MenuBar = ({ routes }: Props) => ( - {routes.map((route, key) => { - const parentPath = `/${route.activityName}`; - - if (!route.children) { - return ( - - ); - } + {routes.map((route) => { + const path = + !route.children + ? `/${route.activityName}` + // $FlowDoNotDisturb difficulty prop exist if the route has children + : `/${route.activityName}/${route.difficulty}/${route.children[0].path}`; return ( - // eslint-disable-next-line react/no-array-index-key - - {route.children.map((child) => { - if (!route.difficulty) { - throw new Error( - // eslint-disable-next-line max-len - `The activity ${route.activityName} requires to add the difficulty to the metadata definition.`, - ); - } - const childrenPath = - `/${route.activityName}/${route.difficulty}/${child.path}`; - - return ( - - ); - })} - + ); })} diff --git a/client/components/pages/links/HeaderLink.jsx b/client/components/pages/links/HeaderLink.jsx index c7c089d..b0d21de 100644 --- a/client/components/pages/links/HeaderLink.jsx +++ b/client/components/pages/links/HeaderLink.jsx @@ -7,22 +7,24 @@ // @see https://github.com/styled-components/stylelint-processor-styled-components/issues/54 // stylelint-disable import React from 'react'; -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink, withRouter } from 'react-router-dom'; import styled from 'styled-components'; import { COLOR_PALETTE } from 'core/constants/colors'; +import gameTextUI from 'core/constants/localize/es/gameTextUI'; const LinkContainer = styled.li` list-style-type: none; position: relative; &::before { + background: ${({ isActive }) => (!isActive ? 'default' : COLOR_PALETTE.orange.clear)}; content: ''; height: 100%; - left: 50%; + left: ${({ isActive }) => (!isActive ? '50%' : '0')}; position: absolute; transition: 0.5s; - width: 0; + width: ${({ isActive }) => (!isActive ? '0' : '100%')}; z-index: -1; } @@ -88,19 +90,33 @@ const ChildrenContainer = styled.ul` } `; +const isActive = (actualUrl: string, url: string) => ( + actualUrl.split('/')[2] === url.split('/')[2] +); +const exerciseTitle = (actualUrl: string) => { + const level = parseInt(actualUrl.split('/').pop()); + + return `: ${gameTextUI.exercise} ${gameTextUI.levels(level)}`; +}; + type Props = {| + location: Object, title: string, + to: string, children?: React.Element, - to?: string, |}; -const HeaderLink = ({ children, to, title }: Props) => ( - - {!to - ? {title} - : {title} - } - {!children || ({children})} - -); +const HeaderLink = ({ children, location, to, title }: Props) => { + const active = isActive(location.pathname, to); + + return ( + + {active + ? {title}{exerciseTitle(location.pathname)} + : {title} + } + {!children || ({children})} + + ); +}; -export default HeaderLink; +export default withRouter(HeaderLink); diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index aa6123e..eb3b2f8 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -1,5 +1,11 @@ ## HEAD +### 💅 React UI + +- ⚙️ [v1.0.2] `Activity`: Adds the `handleNextLevelRedirection` helper (#59). +- ⚙️ [v1.1.0] `CodimoRouter`: Adds a 404 page to announce the end game of an activity (#59). +- 🆕 [v1.0.0] `CodimoRouter/FourOhFour`: 404 page (#59). + ## v1.0.0-alpha.1 ### 🤖 PixiJS Engine diff --git a/core/constants/localize/es/gameTextUI.js b/core/constants/localize/es/gameTextUI.js index 440551f..4f7dbfc 100644 --- a/core/constants/localize/es/gameTextUI.js +++ b/core/constants/localize/es/gameTextUI.js @@ -6,30 +6,33 @@ */ import CompleteURL from '../../images/complete.gif'; +const difficulty = { + easy: 'NIVEL INICIAL', + normal: 'NIVEL INTERMEDIO', + hard: 'NIVEL AVANZADO', +}; + export default { - accept: 'Aceptar', + accept: 'ACEPTAR', loadingMessages: [ - 'Armando la línea numérica', - 'Cazando números salvajes', - 'Levantando los muros del laberinto', - 'Pintando las paredes', - 'Dibujando los bloques', + 'ARMANDO LA LÍNEA NUMÉRICA', + 'CAZANDO NÚMEROS SALVAJES', + 'LEVANTANDO LOS MUROS DEL LABERINTO', + 'PINTANDO LAS PAREDES', + 'DIBUJANDO LOS BLOQUES', ], successMessage: { + confirmButtonText: 'IR AL SIGUIENTE NIVEL', imageUrl: CompleteURL, title: '¡NIVEL COMPLETADO!', - confirmButtonText: 'IR AL SIGUIENTE NIVEL', }, - exercise: 'Ejercicio', - levels: [ - 'Primer', - 'Segundo', - 'Tercer', - 'Cuarto', - ], - difficulty: { - easy: 'Nivel inicial', - normal: 'Nivel intermedio', - hard: 'Nivel avanzado', + endGameMessage: { + title: '¡GENIAL!', + text: (actualDifficulty: string) => ( + `¡TERMINASTE TODOS LOS EJERCICIOS DEL ${difficulty[actualDifficulty]}!` + ), }, + exercise: 'EJERCICIO', + levels: (level: string | number) => `Nº ${level}`, + difficulty, }; diff --git a/core/constants/numbers.js b/core/constants/numbers.js index e0483e2..3e885ed 100644 --- a/core/constants/numbers.js +++ b/core/constants/numbers.js @@ -8,6 +8,7 @@ export const ZERO = 0; export const ONE = 1; export const TWO = 2; +export const THREE = 3; export const FOUR = 4; export const TEN = 10; diff --git a/core/engines/pixijs/components/functionalities/theFallenOneFunctionalityBuilder.test.js b/core/engines/pixijs/components/functionalities/theFallenOneFunctionalityBuilder.test.js index 0d1c9d1..fc1201b 100644 --- a/core/engines/pixijs/components/functionalities/theFallenOneFunctionalityBuilder.test.js +++ b/core/engines/pixijs/components/functionalities/theFallenOneFunctionalityBuilder.test.js @@ -12,7 +12,7 @@ import componentGenerator from '../componentGenerator'; import positioningFunctionalityBuilder from './positioningFunctionalityBuilder'; import theFallenOneFunctionalityBuilder from './theFallenOneFunctionalityBuilder'; -describe('engines/pixijs/components/functionalities/theFallenOneFunctionalityBuilder', async () => { +describe('engines/pixijs/components/functionalities/theFallenOneFunctionalityBuilder', () => { it('should disappear on `beTheFallenOne`', async () => { const initPosition = '1,1'; const size = 64; diff --git a/core/ui/Activity/helpers/handleNextLevelRedirection.js b/core/ui/Activity/helpers/handleNextLevelRedirection.js new file mode 100644 index 0000000..bc0a2b9 --- /dev/null +++ b/core/ui/Activity/helpers/handleNextLevelRedirection.js @@ -0,0 +1,41 @@ +/** + * @author Luciano Graziani @lgraziani2712 + * @license {@link http://www.opensource.org/licenses/mit-license.php|MIT License} + * + * @flow + */ +import swal from 'sweetalert2'; + +import { ONE, THREE } from 'core/constants/numbers'; +import gameTextUI from 'core/constants/localize/es/gameTextUI'; + +/** + * This is in charge of redirect to the next + * exercise in case it exists one. + * If not, it announce the level completeness. + * + * @version 1.0.0 + * @param {string} activityName The activity name. + * @param {string} difficulty The difficulty. + * @param {Object} location The react-router location object. + * @param {Object} history The react-router history object. + * @return {Promise} The sweetalert promise. + */ +const handleNextLevelRedirection = ( + activityName: string, + difficulty: string, + location: Object, + history: Object, +) => (): Promise => { + const level = parseInt(location.pathname.split('/').pop()); + const stringLvl = (level + ONE).toString().padStart(THREE, '0'); + const path = `/${activityName}/${difficulty}/${stringLvl}`; + + return swal(gameTextUI.successMessage).then(() => { + history.push(path); + }, () => { + history.push(path); + }); +}; + +export default handleNextLevelRedirection; diff --git a/core/ui/Activity/index.jsx b/core/ui/Activity/index.jsx index 4df19f2..08fb123 100644 --- a/core/ui/Activity/index.jsx +++ b/core/ui/Activity/index.jsx @@ -6,6 +6,7 @@ */ import React from 'react'; import swal from 'sweetalert2'; +import { withRouter } from 'react-router-dom'; import { type ClientError, @@ -13,7 +14,6 @@ import { import { type Instructions } from 'core/workspaces/blockly/parseInstructions'; import { type GameDifficulty } from 'core/workspaces/blockly/instanciateEveryBlock'; import { type EngineData, type Engine } from 'core/engines/pixijs/engineGenerator'; -import gameTextUI from 'core/constants/localize/es/gameTextUI'; import { ZERO } from 'core/constants/numbers'; import { getRandomInt } from 'core/helpers/randomizers'; @@ -22,6 +22,7 @@ import PixiApp from '../PixiApp'; import BackgroundImage from './components/BackgroundImage'; import TwoColumns from './components/TwoColumns'; +import handleNextLevelRedirection from './helpers/handleNextLevelRedirection'; export type Metadata = {| activityName: string, @@ -34,25 +35,36 @@ type Activity$Props = {| engine: Engine, metadata: Metadata, hasNotEnd?: boolean, + // react-router props + history: Object, + location: Object, + match: Object, |}; /** * The Container is in charge of loading the required activity. * - * @version 1.0.1 + * @version 1.0.2 * @todo 1. Subcomponents async loading. * @todo 2. Make it more generic. * @todo 3. Add example. */ -export default class Activity extends React.Component { +class Activity extends React.Component { props: Activity$Props; image: string; + handleNextLevelRedirection: () => Promise; constructor(props: Activity$Props) { super(props); this.image = props.backgroundImages[getRandomInt(ZERO, props.backgroundImages.length)]; + this.handleNextLevelRedirection = handleNextLevelRedirection( + this.props.metadata.activityName, + this.props.metadata.difficulty, + this.props.location, + this.props.history, + ); } /** * This callback will be used by the BlocklyApp. @@ -75,7 +87,7 @@ export default class Activity extends React.Component { } return executionPromise - .then(() => (swal(gameTextUI.successMessage).catch(swal.noop))) + .then(this.handleNextLevelRedirection) .catch((error: ClientError) => { if (error.title === undefined) { throw error; @@ -108,3 +120,5 @@ export default class Activity extends React.Component { ); } } + +export default withRouter(Activity); diff --git a/core/ui/BlocklyApp/components/BlocklyWorkspace.jsx b/core/ui/BlocklyApp/components/BlocklyWorkspace.jsx index 99fe6f9..adcceca 100644 --- a/core/ui/BlocklyApp/components/BlocklyWorkspace.jsx +++ b/core/ui/BlocklyApp/components/BlocklyWorkspace.jsx @@ -15,12 +15,16 @@ import BlocklyToolbox, { type BlocklyToolboxElement } from './BlocklyToolbox'; const Workspace = styled.div` height: 520px; - width: 580px; + width: 500px; & .blocklyTrash { opacity: 1 !important; } + & .blocklyZoom > image { + opacity: 1; + } + & .blocklyDraggable { cursor: url(${GrabURL}) 15 5, auto; diff --git a/core/ui/BlocklyApp/index.jsx b/core/ui/BlocklyApp/index.jsx index a0197f1..7bb9476 100644 --- a/core/ui/BlocklyApp/index.jsx +++ b/core/ui/BlocklyApp/index.jsx @@ -104,7 +104,10 @@ export default class BlocklyApp extends React.Component { toolbox, trashcan: true, zoom: { - startScale: 1.25, + controls: true, + maxScale: 1.5, + startScale: 1, + scaleSpeed: 1.05, }, }); this.highlightBlock = (id: string) => { diff --git a/core/ui/CodimoRouter/components/FourOhFour.jsx b/core/ui/CodimoRouter/components/FourOhFour.jsx new file mode 100644 index 0000000..1f16073 --- /dev/null +++ b/core/ui/CodimoRouter/components/FourOhFour.jsx @@ -0,0 +1,78 @@ +/** + * @author Luciano Graziani @lgraziani2712 + * @license {@link http://www.opensource.org/licenses/mit-license.php|MIT License} + * + * @flow + */ +import React from 'react'; +import styled from 'styled-components'; + +import gameTextUI from 'core/constants/localize/es/gameTextUI'; + +import EndGameDifficultyURL from '../images/endGameDifficulty.gif'; +import EndGameBackgroundURL from '../images/endGameBackground.jpg'; + +const blurImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+' + + 'AAAAG0lEQVQIW2NkYGD4z8DAwMgABXAGNgGwSgwVAFbmAgXxvZSoAAAAAElFTkSuQmCC'; +const Modal = styled.div` + background: rgb(255, 255, 255); + display: block; + min-height: 316px; + padding: 0 20px 20px; + width: 630px; +`; +const ModalContainer = styled.div` + align-items: center; + display: flex; + height: 100%; + + &::before { + background: url(${EndGameBackgroundURL}); + background-size: cover; + content: ''; + height: 100%; + position: absolute; + top: 0; + width: 100%; + z-index: -1; + } + + &::after { + background: url(${blurImage}) repeat; + content: ''; + height: 100%; + position: absolute; + top: 0; + width: 100%; + z-index: -1; + } +`; +const ModalImage = styled.img` + display: block; +`; + +type FourOhFour$Props = {| + location: Object, +|}; +/** + * The temporal End Game announcer. + * + * @version 1.0.0 + * @param {Object} location React-router location object. + * @constructor + */ +export default function FourOhFour({ location }: FourOhFour$Props) { + const actualDifficulty = location.pathname.split('/')[2]; + + return ( + + + +

{gameTextUI.endGameMessage.title}

+
+ {gameTextUI.endGameMessage.text(actualDifficulty)} +
+
+
+ ); +} diff --git a/core/ui/CodimoRouter/components/Snapshot.test.jsx b/core/ui/CodimoRouter/components/Snapshot.test.jsx new file mode 100644 index 0000000..1971ab9 --- /dev/null +++ b/core/ui/CodimoRouter/components/Snapshot.test.jsx @@ -0,0 +1,26 @@ +/** + * @author Luciano Graziani @lgraziani2712 + * @license {@link http://www.opensource.org/licenses/mit-license.php|MIT License} + * + * @flow + * @jest-environment node + */ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import 'test/BlocklyMock'; +import 'jest-styled-components'; + +import FourOhFour from './FourOhFour'; + +describe('CodimoRouter components snapshots', () => { + it('renders the raw components correctly', () => { + const fourOhFourTree = renderer.create( + , + ).toJSON(); + + expect(fourOhFourTree).toMatchSnapshot(); + }); +}); diff --git a/core/ui/CodimoRouter/components/__snapshots__/Snapshot.test.jsx.snap b/core/ui/CodimoRouter/components/__snapshots__/Snapshot.test.jsx.snap new file mode 100644 index 0000000..3f836f5 --- /dev/null +++ b/core/ui/CodimoRouter/components/__snapshots__/Snapshot.test.jsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CodimoRouter components snapshots renders the raw components correctly 1`] = ` +.c1 { + background: rgb(255,255,255); + display: block; + min-height: 316px; + padding: 0 20px 20px; + width: 630px; +} + +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + height: 100%; +} + +.c0::before { + background: url(endGameBackground.jpg); + background-size: cover; + content: ''; + height: 100%; + position: absolute; + top: 0; + width: 100%; + z-index: -1; +} + +.c0::after { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAG0lEQVQIW2NkYGD4z8DAwMgABXAGNgGwSgwVAFbmAgXxvZSoAAAAAElFTkSuQmCC) repeat; + content: ''; + height: 100%; + position: absolute; + top: 0; + width: 100%; + z-index: -1; +} + +.c2 { + display: block; +} + +
+
+ +

+ ¡GENIAL! +

+
+ ¡TERMINASTE TODOS LOS EJERCICIOS DEL undefined! +
+
+
+`; diff --git a/core/ui/CodimoRouter/images/README.md b/core/ui/CodimoRouter/images/README.md new file mode 100644 index 0000000..3f9ecc2 --- /dev/null +++ b/core/ui/CodimoRouter/images/README.md @@ -0,0 +1,11 @@ +# Original links + +- `endGameDifficulty.gif`: + - Author: Mauro Gatti (https://giphy.com/maurogatti/). + - Original link to gif: https://media.giphy.com/media/l41lUjUgLLwWrz20w/giphy.gif. + - Original webpage link: https://giphy.com/gifs/cool-ok-sure-l41lUjUgLLwWrz20w/. + +- `endGameBackground.jpg`: + - Author: Unknown. + - Original link to gif: http://www.hanoverinn.co.uk/wp-content/uploads/2016/11/Fireworks.png. + - Original webpage link: https://dynamicfireworks.co.uk/10-uks-amazing-bonfire-night-fireworks-displays/. diff --git a/core/ui/CodimoRouter/images/endGameBackground.jpg b/core/ui/CodimoRouter/images/endGameBackground.jpg new file mode 100644 index 0000000..be38800 Binary files /dev/null and b/core/ui/CodimoRouter/images/endGameBackground.jpg differ diff --git a/core/ui/CodimoRouter/images/endGameDifficulty.gif b/core/ui/CodimoRouter/images/endGameDifficulty.gif new file mode 100644 index 0000000..7735264 Binary files /dev/null and b/core/ui/CodimoRouter/images/endGameDifficulty.gif differ diff --git a/core/ui/CodimoRouter/index.jsx b/core/ui/CodimoRouter/index.jsx index 7e32977..4e62330 100644 --- a/core/ui/CodimoRouter/index.jsx +++ b/core/ui/CodimoRouter/index.jsx @@ -5,9 +5,10 @@ * @flow */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import activityLoader from './activityLoader'; +import FourOhFour from './components/FourOhFour'; export type Codimo$Route = {| activityName: string, @@ -25,44 +26,53 @@ type CodimoRouter$Props = {| /** * This component is responsible for loading asynchronously the required - * activity. Each activity has a unique path based + * activity. * - * @version 1.0.0 - * @param {CodimoRouter$Props} routes A list of description of a Route. - * @return {Array>} A list of the routes and its components + * @version 1.1.0 + * @param {CodimoRouter$Props} routes A list of description of a Route. + * @constructor */ export default function CodimoRouter({ routes }: CodimoRouter$Props) { - return routes.map((route) => { - if (!route.children) { - const path = `/${route.activityName}`; + return ( + + {routes.map((route) => { + if (!route.children) { + const path = `/${route.activityName}`; - return ( - - ); - } + return ( + + ); + } - return route.children.map((child) => { - if (!route.difficulty) { - throw new Error( - // eslint-disable-next-line max-len - `The activity ${route.activityName} requires to add the difficulty to the metadata definition.`, - ); - } - const path = `/${route.activityName}/${route.difficulty}/${child.path}`; + return route.children.map((child) => { + if (!route.difficulty) { + throw new Error( + // eslint-disable-next-line max-len + `The activity ${route.activityName} requires to add the difficulty to the metadata definition.`, + ); + } + const path = `/${route.activityName}/${route.difficulty}/${child.path}`; - return ( - - ); - }); - }); + return ( + + ); + }); + })} + {/* + * 404 for exercises. This means the game has ended. + * TODO 404 for exercises needs a proper refactor. + */} + + + ); } diff --git a/core/ui/PixiApp/index.jsx b/core/ui/PixiApp/index.jsx index 6ecb281..e9ec774 100644 --- a/core/ui/PixiApp/index.jsx +++ b/core/ui/PixiApp/index.jsx @@ -46,7 +46,7 @@ export default class PixiApp extends React.Component { // TODO destroy app? reset on new props componentWillUnmount() { this.app.stop(); - this.app.destroy(true); + this.app.destroy(); } render() { return this.view = view} />; diff --git a/dev-elements/test/PixiWrapper.jsx b/dev-elements/test/PixiWrapper.jsx index 815f887..b2b62c9 100644 --- a/dev-elements/test/PixiWrapper.jsx +++ b/dev-elements/test/PixiWrapper.jsx @@ -38,7 +38,7 @@ export default class PixiWrapper extends React.Component { } componentWillUnmount() { this.app.stop(); - this.app.destroy(true); + this.app.destroy(); } render() { return this.view = view} />; diff --git a/package.json b/package.json index 42d3351..a061b00 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "flow:check": "flow check", "flow:start": "flow start", "flow:stop": "flow stop", + "flow-typed": "flow-typed update", "lint": "eslint core activities client dev-elements .dev-tools --ext .js --ext .jsx --fix", "lint:css": "stylelint activities/**/*.jsx activities/**/*.js client/**/*.jsx client/**/*.js core/**/*.jsx core/**/*.js", "start": "webpack-dev-server --progress --config ./.dev-tools/config/webpack.config.dev.js",