├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── components │ ├── App.js │ ├── Box.js │ ├── BoxDraggable.js │ ├── Canvas.js │ └── Toolbar.js ├── index.js ├── main.css ├── serviceWorker.js ├── setupTests.js ├── stores │ ├── MainStore.js │ └── models │ │ └── Box.js ├── tests │ └── App.test.js └── utils │ └── getRandomColor.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Welcome to this code test! :) 2 | 3 | The main objective of this technical excercise is for you to get a good grasp of what kind of problems we encounter on Genially. We wouldn't want you to find some nasty surprises if you decide to join us. Also, it's a good starting point to have a technical conversation during an interview. 4 | 5 | # Technology included 6 | 7 | As you can see, the code test is a simple create-react-app, with some included libraries and some code bundled with it. Let's go through some of the lesser-known technologies. 8 | 9 | ## mobx-state-tree (MST for short) 10 | 11 | This is the app state manager we use at our React apps. It's meant to be used with mobx, and unlike it, is very opinionated as how you should define your stores, models etc. 12 | 13 | https://github.com/mobxjs/mobx-state-tree 14 | 15 | ## interact.js 16 | 17 | Genially is a very interactivity-heavy application. Almost everything you use on the app can be moved around with your mouse, selected, scaled, rotated, etc. This library does most of the heavy lifting for us. 18 | 19 | https://interactjs.io/ 20 | 21 | # Test requirements 22 | 23 | The test is an extremely simplified version of the Genially editor. We provide you a working area, named `Canvas`, and elements that are displayed inside of it, named `Box`. 24 | 25 | We've also added a rudimentary toolbar for some of the required functionality. 26 | 27 | When finished, the app should let the user: 28 | 29 | - Add and remove boxes. 30 | - Select a box, which should visually indicate that is selected 31 | - Drag the boxes around using interact.js and using React refs. 32 | - Keep in mind you should be able to drag a box even if it's not selected when the dragging starts. 33 | - Changing a box's color. 34 | - Display a counter indicating how many boxes are selected. 35 | - Support selection, dragging and color changing for multiple boxes. 36 | - Save the state of the app locally and restore it when it loads. 37 | - Undo / Redo capabilities 38 | - **hint**: mobx-state-tree provides a middleware for this. 39 | 40 | If you are unable to do some of the above, don't worry! But we would ask to at least explain what went wrong, how you would tackle the problem, or if you have no idea whatsoever 😃 41 | 42 | Even if you manage to do everything, we also greatly appreciate comments on decisions you took, issues you faced or limitations you've left behind on purpose. 43 | 44 | A good place to include those comments is the README.md of the repo. 45 | 46 | # Delivery method 47 | 48 | Send it to us however it suits you, but our preferred method is to get access to a **private fork of the repo**. This way, we can see historical changes, and a complete diff against the original repo on a PR. It's also a great way to write down feedback and discussion points for the interview afterwards. 49 | 50 | If you opt for a fork with limited access, see the contact list below for people you can give access to. Please always include Chema & Román, and then someone else (or all of them!). 51 | 52 | # Contact 53 | 54 | If you have any questions about the test, you can contact any of us: 55 | 56 | - Chema (Github User [@chemitaxis](https://github.com/chemitaxis) / chema@genially.com) 57 | - Rafa (rafa@genially.com) 58 | - Emanuel (emanuel@genially.com) 59 | - Jesé (jese@genially.com) 60 | - Román (roman@genially.com) 61 | - Perico (perico@genially.com) 62 | - Julio (juboba@genially.com) 63 | 64 | Good Luck! 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-test", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "4.2.4", 7 | "@testing-library/react": "9.3.2", 8 | "@testing-library/user-event": "7.1.2", 9 | "interactjs": "1.8.4", 10 | "mobx": "5.15.4", 11 | "mobx-react": "6.1.4", 12 | "mobx-state-tree": "3.15.0", 13 | "mst-middlewares": "3.15.0", 14 | "react": "16.12.0", 15 | "react-dom": "16.12.0", 16 | "react-scripts": "3.3.1", 17 | "uuid": "3.4.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Genially/frontend-code-test/5ac06937ba8b61b973bfd7315402e60e840b4fc7/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Genially/frontend-code-test/5ac06937ba8b61b973bfd7315402e60e840b4fc7/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Genially/frontend-code-test/5ac06937ba8b61b973bfd7315402e60e840b4fc7/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import store from "../stores/MainStore"; 4 | import Canvas from "./Canvas"; 5 | import Toolbar from "./Toolbar"; 6 | import { observer } from "mobx-react"; 7 | 8 | function App() { 9 | return ( 10 |
11 | 12 | 13 |
14 | ); 15 | } 16 | 17 | export default observer(App); 18 | -------------------------------------------------------------------------------- /src/components/Box.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { observer } from "mobx-react"; 3 | import BoxDraggable from "./BoxDraggable"; 4 | 5 | function Box(props) { 6 | return ( 7 | 8 |
Box
9 |
10 | ); 11 | } 12 | 13 | export default observer(Box); 14 | -------------------------------------------------------------------------------- /src/components/BoxDraggable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { observer } from "mobx-react"; 3 | 4 | function BoxDraggable(props) { 5 | return ( 6 |
16 | {props.children} 17 |
18 | ); 19 | } 20 | 21 | export default observer(BoxDraggable); 22 | -------------------------------------------------------------------------------- /src/components/Canvas.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { observer } from "mobx-react"; 4 | import Box from "../components/Box"; 5 | 6 | function Canvas({ store }) { 7 | return ( 8 |
9 | {store.boxes.map((box, index) => ( 10 | 20 | ))} 21 |
22 | ); 23 | } 24 | 25 | export default observer(Canvas); 26 | -------------------------------------------------------------------------------- /src/components/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Toolbar() { 4 | return ( 5 |
6 | 7 | 8 | 9 | No boxes selected 10 |
11 | ); 12 | } 13 | 14 | export default Toolbar; 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./main.css"; 4 | import App from "./components/App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | body, 2 | html, 3 | #root { 4 | background-color: #282c34; 5 | width: 100%; 6 | height: 100%; 7 | margin: 0; 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 9 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 10 | sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | .app { 15 | width: 100%; 16 | height: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | justify-content: center; 21 | } 22 | .canva { 23 | width: 1200px; 24 | height: 675px; 25 | background-color: aliceblue; 26 | } 27 | 28 | .box { 29 | position: absolute; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | } 34 | 35 | .toolbar { 36 | position: fixed; 37 | top: 0; 38 | left: 0; 39 | display: flex; 40 | } 41 | 42 | .toolbar span { 43 | color: white; 44 | } 45 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/stores/MainStore.js: -------------------------------------------------------------------------------- 1 | import { types } from "mobx-state-tree"; 2 | import uuid from "uuid/v4"; 3 | import BoxModel from "./models/Box"; 4 | import getRandomColor from "../utils/getRandomColor"; 5 | 6 | const MainStore = types 7 | .model("MainStore", { 8 | boxes: types.array(BoxModel) 9 | }) 10 | .actions(self => { 11 | return { 12 | addBox(box) { 13 | self.boxes.push(box); 14 | } 15 | }; 16 | }) 17 | .views(self => ({})); 18 | 19 | const store = MainStore.create(); 20 | 21 | const box1 = BoxModel.create({ 22 | id: uuid(), 23 | color: getRandomColor(), 24 | left: 0, 25 | top: 0 26 | }); 27 | 28 | store.addBox(box1); 29 | 30 | export default store; 31 | -------------------------------------------------------------------------------- /src/stores/models/Box.js: -------------------------------------------------------------------------------- 1 | import { types } from "mobx-state-tree"; 2 | 3 | const BoxModel = types 4 | .model("Box", { 5 | id: types.identifier, 6 | width: 200, 7 | height: 100, 8 | color: "#FFF000", 9 | left: 200, 10 | top: 100 11 | }) 12 | .views(self => ({})) 13 | .actions(self => ({})); 14 | 15 | export default BoxModel; 16 | -------------------------------------------------------------------------------- /src/tests/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "../components/App"; 4 | 5 | test("Renders correctly the app", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/utils/getRandomColor.js: -------------------------------------------------------------------------------- 1 | const getRandomColor = () => { 2 | const letters = "0123456789ABCDEF"; 3 | let color = "#"; 4 | for (let i = 0; i < 6; i++) { 5 | color += letters[Math.floor(Math.random() * 16)]; 6 | } 7 | return color; 8 | }; 9 | 10 | export default getRandomColor; 11 | --------------------------------------------------------------------------------