MILAN 25-26 NOVEMBER 2016 { Universal JS Web Applications with React Luciano Mammino 1
WHO IS LUCIANO? nodejsdesignpatterns.com lmammino loige loige.co 2
AGENDA 1. The term "Universal" JS 2. Who & Why 3. Common problems and technologies 4. Building a frontend only Single Page App 5. Making it universal 3
ISOMORPHIC bit.ly/universaljs UNIVERSAL WHAT? 4
NOT ONLY FOR THE WEB... Desktop applications Mobile applications Hardware 5
ADVANTAGES OF UNIVERSAL JAVASCRIPT "JavaScript-only" development Maintainability Better SEO Faster "perceived" load time 6
IN THE WILD 7
IT LOOKS GREAT BUT... 8
MODULE SHARING Use Node.js modules in the browser. UMD 9
UNIVERSAL RENDERING Render the views of the application from the server (first request) and then in the browser (next requests) 10
UNIVERSAL ROUTING Recognise the view associated to the current route from both the server and the browser. 11
UNIVERSAL DATA RETRIEVAL Access data (and APIs) from both the server and the browser. AXIOS UNIVERSAL FETCH 12
UNIVERSAL STATE MANAGEMENT Manage changes on the state tree both on the server and the client... 13
FUTURISTIC/ALTERNATIVE JS?! 14
15
OK... LET'S STOP COMPLAINING AND BUILD SOMETHING! 16
WHAT TOOLS ARE WE GOING TO USE? 17
WHAT ARE WE GOING TO BUILD? judo-heroes.herokuapp.com bit.ly/judo-heroes-tutorial 18
19
20
21
curl -sS "https://judo-heroes.herokuapp.com/athlete/teddy-riner" 22
The data set // src/data/athletes.js const athletes = [ { 'id': 'driulis-gonzalez', 'name': 'Driulis González', 'country': { 'id': 'cu', 'name': 'Cuba', 'icon': 'flag-cu.png', }, 'birth': '1973', 'image': 'driulis-gonzalez.jpg', 'cover': 'driulis-gonzalez-cover.jpg', 'link': 'https://en.wikipedia.org/wiki/Driulis_González', 'medals': [ { 'year': '1992', 'type': 'B', 'city': 'Barcelona', 'event': 'Olympic Games', 'category': '-57kg { 'year': '1993', 'type': 'B', 'city': 'Hamilton', 'event': 'World Championships', 'category': ' { 'year': '1995', 'type': 'G', 'city': 'Chiba', 'event': 'World Championships', 'category': '-57 { 'year': '1995', 'type': 'G', 'city': 'Mar del Plata', 'event': 'Pan American Games', 'category { 'year': '1996', 'type': 'G', 'city': 'Atlanta', 'event': 'Olympic Games', 'category': '-57kg' // ... ], }, // ... ]; export default athletes; 23
REACT COMPONENTS 24
Layout component 25
// src/components/Layout.js import React from 'react'; import { Link } from 'react-router'; const Layout = (props) => ( <div className="app-container"> <header> <Link to="/"> <img className="logo" src="/img/logo-judo-heroes.png"/> </Link> </header> <div className="app-content">{props.children}</div> <footer> <p> This is a demo app to showcase <strong>universal Javascript</strong> with <strong>React</strong> and <strong>Express</strong>. </p> </footer> </div> ); export default Layout; 26
IndexPage component 27
// src/components/IndexPage.js import React from 'react'; import AthletePreview from './AthletePreview'; import athletes from '../data/athletes'; const IndexPage = (props) => ( <div className="home"> <div className="athletes-selector"> {athletes.map( athleteData => <AthletePreview key={athleteData.id} {...athleteData} /> )} </div> </div> ); export default IndexPage; 28
AthletePreview component 29
// src/components/AthletePreview.js import React from 'react'; import { Link } from 'react-router'; const AthletePreview = (props) => ( <Link to={`/athlete/${props.id}`}> <div className="athlete-preview"> <img src={`img/${props.image}`}/> <h2 className="name">{props.name}</h2> <span className="medals-count"> <img src="/img/medal.png"/> {props.medals.length} </span> </div> </Link> ); export default AthletePreview; 30
AthletePage component 31
// src/components/AthletePage.js import React from 'react'; import { Link } from 'react-router'; import NotFoundPage from './NotFoundPage'; import AthletesMenu from './AthletesMenu'; import Medal from './Medal'; import Flag from './Flag'; import athletes from '../data/athletes'; const AthletePage = (props) => { const id = props.params.id; const athlete = athletes.find((athlete) => athlete.id === id); if (!athlete) { return <NotFoundPage/>; } const headerStyle = { backgroundImage: `url(/img/${athlete.cover})` }; return ( <div className="athlete-full"> <AthletesMenu/> <div className="athlete"> <header style={headerStyle}/> <div className="picture-container"> <img src={`/img/${athlete.image}`}/> <h2 className="name">{athlete.name}</h2> </div> <section className="description"> Olympic medalist from <strong><Flag {...athlete.country} showName="true"/></strong>, born in {athlete.birth} (Find out more on <a href={athlete.link} target="_blank">Wikipedia</a>). </section> <section className="medals"> <p>Winner of <strong>{athlete.medals.length}</strong> medals:</p> <ul>{ athlete.medals.map((medal, i) => <Medal key={i} {...medal}/>) }</ul> </section> </div> <div className="navigateBack"> <Link to="/">« Back to the index</Link> </div> </div> ); }; export default AthletePage; 32
// src/components/AthletePage.js // ... const AthletePage = (props) => { const id = props.params.id; const athlete = athletes.find((athlete) => athlete.id === id); if (!athlete) { return <NotFoundPage/>; } const headerStyle = { backgroundImage: `url(/img/${athlete.cover})` }; return ( <div className="athlete-full"> <AthletesMenu/> <div className="athlete"> <header style={headerStyle}/> <div className="picture-container"> <img src={`/img/${athlete.image}`}/> <h2 className="name">{athlete.name}</h2> </div> // ... 33
// src/components/AthletePage.js // ... <section className="description"> Olympic medalist from <strong><Flag {...athlete.country} showName="true"/></strong>, born in {athlete.birth} (Find out more on <a href={athlete.link} target="_blank">Wikipedia</a>). </section> <section className="medals"> <p>Winner of <strong>{athlete.medals.length}</strong> medals:</p> <ul>{ athlete.medals.map((medal, i) => <Medal key={i} {...medal}/>) }</ul> </section> </div> <div className="navigateBack"> <Link to="/">« Back to the index</Link> </div> </div> ); }; export default AthletePage; 34
AthletesMenu component 35
// src/components/AthletesMenu.js import React from 'react'; import { Link } from 'react-router'; import athletes from '../data/athletes'; const AthletesMenu = (props) => ( <nav className="atheletes-menu"> {athletes.map(athlete => { return <Link key={athlete.id} to={`/athlete/${athlete.id}`} activeClassName="active"> {athlete.name} </Link>; })} </nav> ); export default AthletesMenu; 36
Flag component 37
// src/components/Flag.js import React from 'react'; const Flag = (props) => ( <span className="flag"> <img className="icon" title={props.name} src={`/img/${props.icon}`}/> {props.showName && <span className="name"> {props.name}</span>} </span> ); export default Flag; 38
Medal component 39
// src/components/Medal.js import React from 'react'; const medalTypes = { 'G': 'Gold', 'S': 'Silver', 'B': 'Bronze' }; const Medal = (props) => ( <li className="medal"> <span className={`symbol symbol-${props.type}`} title={medalTypes[props.type]}> {props.type} </span> <span className="year">{props.year}</span> <span className="city"> {props.city}</span> <span className="event"> ({props.event})</span> <span className="category"> {props.category}</span> </li> ); export default Medal; 40
NotFoundPage component // src/components/NotFoundPage.js import React from 'react'; import { Link } from 'react-router'; const NotFoundPage = (props) => ( <div className="not-found"> <h1>404</h1> <h2>Page not found!</h2> <p> <Link to="/">Go back to the main page</Link> </p> </div> ); export default NotFoundPage; 41
ROUTING 42
2 ROUTES Index Page: / Athlete Page: /athlete/:id 43
// src/Routes.js import React from 'react'; import { Route, IndexRoute } from 'react-router' import Layout from './components/Layout'; import IndexPage from './components/IndexPage'; import AthletePage from './components/AthletePage'; import NotFoundPage from './components/NotFoundPage'; const routes = ( <Route path="/" component={Layout}> <IndexRoute component={IndexPage}/> <Route path="athlete/:id" component={AthletePage}/> <Route path="*" component={NotFoundPage}/> </Route> ); export default routes; 44
// src/components/AppRoutes.js import React from 'react'; import { Router, hashHistory } from 'react-router'; import routes from '../Routes'; const AppRoutes = (props) => ( <Router history={hashHistory} routes={routes} onUpdate={() => window.scrollTo(0, 0)}/> ); export default AppRoutes; 45
CLIENT APP 46
// src/app-client.js import React from 'react'; import ReactDOM from 'react-dom'; import AppRoutes from './components/AppRoutes'; window.onload = () => { ReactDOM.render(<AppRoutes/>, document.getElementById('main')); }; 47
HTML WRAPPER 48
// src/static/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Judo Heroes - A Universal JavaScript demo application with React</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <div id="main"></div> <script src="/js/bundle.js"></script> </body> </html> 49
BUILD CONFIG (BABEL + WEBPACK) 50
.babelrc import webpack from 'webpack'; import path from 'path'; const config = { entry: { js: './src/app-client.js' }, output: { path: path.join(__dirname, 'src', 'static', 'js'), filename: 'bundle.js' }, module: { loaders: [{ test: path.join(__dirname, 'src'), loaders: [{ loader: 'babel-loader' }] }] }, }; export default config; .webpack.config.babel.js { "presets": ["react", "es2015"] } 51
LET'S BUILD IT! 52
// src/server.js import path from 'path'; import { Server } from 'http'; import Express from 'express'; const app = new Express(); const server = new Server(app); // define the folder that will be used for static assets app.use(Express.static(path.join(__dirname, 'static'))); // start the server const port = process.env.PORT || 3000; const env = process.env.NODE_ENV || 'production'; server.listen(port, err => { if (err) { return console.error(err); } console.info(`Server running on http://localhost:${port} [${env}]`); }); Static Express server 53
READY... LET'S TEST IT 54
RECAP What we learned so far 1. Define views combining React components 2. Add Routing using React Router 3. Compiling our frontend bundle with Babel and Webpack 4. Run the app with a static Express server 55
SERVER SIDE RENDERING AND ROUTING 56
Using browser history 57
// src/views/index.ejs <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Judo Heroes - A Universal JavaScript demo application with React</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <div id="main"><%- markup -%></div> <script src="/js/bundle.js"></script> </body> </html> Converting static index.html into a template 58
Updating the server app 59
// ... // universal routing and rendering app.get('*', (req, res) => { match( { routes, location: req.url }, (err, redirectLocation, renderProps) => { // in case of error display the error message if (err) { return res.status(500).send(err.message); } // in case of redirect propagate the redirect to the browser if (redirectLocation) { return res.redirect(302, redirectLocation.pathname + redirectLocation.search); } // generate the React markup for the current route let markup; if (renderProps) { // if the current route matched we have renderProps markup = renderToString(<RouterContext {...renderProps}/>); } else { // otherwise we can render a 404 page markup = renderToString(<NotFoundPage/>); res.status(404); } // render the index template with the embedded React markup return res.render('index', { markup }); } ); }); 60
THAT'S IT! LET'S TEST AGAIN 61
RECAP What we learned so far 1. Create a Single Page Application with React and React Router 2. Add server side routing and rendering using React and React Router libraries in out Express app 62
UNIVERSAL DATA RETRIEVAL api-proxy & async-props (COMPLETE CHAPTER in ) UNIVERSAL STATE MANAGEMENT Redux Node.js Design Patterns WHERE DO WE GO from here... Code: https://github.com/lmammino/judo-heroes-2 63
THANKS! loige loige.colmammino (Special thanks to , , Aleksandar Čambas & )@cirpo @andreaman87 @quasi_modal 64

Universal JavaScript Web Applications with React - Luciano Mammino - Codemotion Milan 2016

  • 1.
    MILAN 25-26 NOVEMBER2016 { Universal JS Web Applications with React Luciano Mammino 1
  • 2.
  • 3.
    AGENDA 1. The term"Universal" JS 2. Who & Why 3. Common problems and technologies 4. Building a frontend only Single Page App 5. Making it universal 3
  • 4.
  • 5.
    NOT ONLY FOR THEWEB... Desktop applications Mobile applications Hardware 5
  • 6.
    ADVANTAGES OF UNIVERSAL JAVASCRIPT "JavaScript-only"development Maintainability Better SEO Faster "perceived" load time 6
  • 7.
  • 8.
  • 9.
    MODULE SHARING Use Node.jsmodules in the browser. UMD 9
  • 10.
    UNIVERSAL RENDERING Render theviews of the application from the server (first request) and then in the browser (next requests) 10
  • 11.
    UNIVERSAL ROUTING Recognise theview associated to the current route from both the server and the browser. 11
  • 12.
    UNIVERSAL DATA RETRIEVAL Accessdata (and APIs) from both the server and the browser. AXIOS UNIVERSAL FETCH 12
  • 13.
    UNIVERSAL STATE MANAGEMENT Manage changeson the state tree both on the server and the client... 13
  • 14.
  • 15.
  • 16.
  • 17.
    WHAT TOOLS ARE WEGOING TO USE? 17
  • 18.
    WHAT ARE WE GOINGTO BUILD? judo-heroes.herokuapp.com bit.ly/judo-heroes-tutorial 18
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
    The data set //src/data/athletes.js const athletes = [ { 'id': 'driulis-gonzalez', 'name': 'Driulis González', 'country': { 'id': 'cu', 'name': 'Cuba', 'icon': 'flag-cu.png', }, 'birth': '1973', 'image': 'driulis-gonzalez.jpg', 'cover': 'driulis-gonzalez-cover.jpg', 'link': 'https://en.wikipedia.org/wiki/Driulis_González', 'medals': [ { 'year': '1992', 'type': 'B', 'city': 'Barcelona', 'event': 'Olympic Games', 'category': '-57kg { 'year': '1993', 'type': 'B', 'city': 'Hamilton', 'event': 'World Championships', 'category': ' { 'year': '1995', 'type': 'G', 'city': 'Chiba', 'event': 'World Championships', 'category': '-57 { 'year': '1995', 'type': 'G', 'city': 'Mar del Plata', 'event': 'Pan American Games', 'category { 'year': '1996', 'type': 'G', 'city': 'Atlanta', 'event': 'Olympic Games', 'category': '-57kg' // ... ], }, // ... ]; export default athletes; 23
  • 24.
  • 25.
  • 26.
    // src/components/Layout.js import Reactfrom 'react'; import { Link } from 'react-router'; const Layout = (props) => ( <div className="app-container"> <header> <Link to="/"> <img className="logo" src="/img/logo-judo-heroes.png"/> </Link> </header> <div className="app-content">{props.children}</div> <footer> <p> This is a demo app to showcase <strong>universal Javascript</strong> with <strong>React</strong> and <strong>Express</strong>. </p> </footer> </div> ); export default Layout; 26
  • 27.
  • 28.
    // src/components/IndexPage.js import Reactfrom 'react'; import AthletePreview from './AthletePreview'; import athletes from '../data/athletes'; const IndexPage = (props) => ( <div className="home"> <div className="athletes-selector"> {athletes.map( athleteData => <AthletePreview key={athleteData.id} {...athleteData} /> )} </div> </div> ); export default IndexPage; 28
  • 29.
  • 30.
    // src/components/AthletePreview.js import Reactfrom 'react'; import { Link } from 'react-router'; const AthletePreview = (props) => ( <Link to={`/athlete/${props.id}`}> <div className="athlete-preview"> <img src={`img/${props.image}`}/> <h2 className="name">{props.name}</h2> <span className="medals-count"> <img src="/img/medal.png"/> {props.medals.length} </span> </div> </Link> ); export default AthletePreview; 30
  • 31.
  • 32.
    // src/components/AthletePage.js import Reactfrom 'react'; import { Link } from 'react-router'; import NotFoundPage from './NotFoundPage'; import AthletesMenu from './AthletesMenu'; import Medal from './Medal'; import Flag from './Flag'; import athletes from '../data/athletes'; const AthletePage = (props) => { const id = props.params.id; const athlete = athletes.find((athlete) => athlete.id === id); if (!athlete) { return <NotFoundPage/>; } const headerStyle = { backgroundImage: `url(/img/${athlete.cover})` }; return ( <div className="athlete-full"> <AthletesMenu/> <div className="athlete"> <header style={headerStyle}/> <div className="picture-container"> <img src={`/img/${athlete.image}`}/> <h2 className="name">{athlete.name}</h2> </div> <section className="description"> Olympic medalist from <strong><Flag {...athlete.country} showName="true"/></strong>, born in {athlete.birth} (Find out more on <a href={athlete.link} target="_blank">Wikipedia</a>). </section> <section className="medals"> <p>Winner of <strong>{athlete.medals.length}</strong> medals:</p> <ul>{ athlete.medals.map((medal, i) => <Medal key={i} {...medal}/>) }</ul> </section> </div> <div className="navigateBack"> <Link to="/">« Back to the index</Link> </div> </div> ); }; export default AthletePage; 32
  • 33.
    // src/components/AthletePage.js // ... constAthletePage = (props) => { const id = props.params.id; const athlete = athletes.find((athlete) => athlete.id === id); if (!athlete) { return <NotFoundPage/>; } const headerStyle = { backgroundImage: `url(/img/${athlete.cover})` }; return ( <div className="athlete-full"> <AthletesMenu/> <div className="athlete"> <header style={headerStyle}/> <div className="picture-container"> <img src={`/img/${athlete.image}`}/> <h2 className="name">{athlete.name}</h2> </div> // ... 33
  • 34.
    // src/components/AthletePage.js // ... <sectionclassName="description"> Olympic medalist from <strong><Flag {...athlete.country} showName="true"/></strong>, born in {athlete.birth} (Find out more on <a href={athlete.link} target="_blank">Wikipedia</a>). </section> <section className="medals"> <p>Winner of <strong>{athlete.medals.length}</strong> medals:</p> <ul>{ athlete.medals.map((medal, i) => <Medal key={i} {...medal}/>) }</ul> </section> </div> <div className="navigateBack"> <Link to="/">« Back to the index</Link> </div> </div> ); }; export default AthletePage; 34
  • 35.
  • 36.
    // src/components/AthletesMenu.js import Reactfrom 'react'; import { Link } from 'react-router'; import athletes from '../data/athletes'; const AthletesMenu = (props) => ( <nav className="atheletes-menu"> {athletes.map(athlete => { return <Link key={athlete.id} to={`/athlete/${athlete.id}`} activeClassName="active"> {athlete.name} </Link>; })} </nav> ); export default AthletesMenu; 36
  • 37.
  • 38.
    // src/components/Flag.js import Reactfrom 'react'; const Flag = (props) => ( <span className="flag"> <img className="icon" title={props.name} src={`/img/${props.icon}`}/> {props.showName && <span className="name"> {props.name}</span>} </span> ); export default Flag; 38
  • 39.
  • 40.
    // src/components/Medal.js import Reactfrom 'react'; const medalTypes = { 'G': 'Gold', 'S': 'Silver', 'B': 'Bronze' }; const Medal = (props) => ( <li className="medal"> <span className={`symbol symbol-${props.type}`} title={medalTypes[props.type]}> {props.type} </span> <span className="year">{props.year}</span> <span className="city"> {props.city}</span> <span className="event"> ({props.event})</span> <span className="category"> {props.category}</span> </li> ); export default Medal; 40
  • 41.
    NotFoundPage component // src/components/NotFoundPage.js importReact from 'react'; import { Link } from 'react-router'; const NotFoundPage = (props) => ( <div className="not-found"> <h1>404</h1> <h2>Page not found!</h2> <p> <Link to="/">Go back to the main page</Link> </p> </div> ); export default NotFoundPage; 41
  • 42.
  • 43.
    2 ROUTES Index Page:/ Athlete Page: /athlete/:id 43
  • 44.
    // src/Routes.js import Reactfrom 'react'; import { Route, IndexRoute } from 'react-router' import Layout from './components/Layout'; import IndexPage from './components/IndexPage'; import AthletePage from './components/AthletePage'; import NotFoundPage from './components/NotFoundPage'; const routes = ( <Route path="/" component={Layout}> <IndexRoute component={IndexPage}/> <Route path="athlete/:id" component={AthletePage}/> <Route path="*" component={NotFoundPage}/> </Route> ); export default routes; 44
  • 45.
    // src/components/AppRoutes.js import Reactfrom 'react'; import { Router, hashHistory } from 'react-router'; import routes from '../Routes'; const AppRoutes = (props) => ( <Router history={hashHistory} routes={routes} onUpdate={() => window.scrollTo(0, 0)}/> ); export default AppRoutes; 45
  • 46.
  • 47.
    // src/app-client.js import Reactfrom 'react'; import ReactDOM from 'react-dom'; import AppRoutes from './components/AppRoutes'; window.onload = () => { ReactDOM.render(<AppRoutes/>, document.getElementById('main')); }; 47
  • 48.
  • 49.
    // src/static/index.html <!DOCTYPE html> <html> <head> <metacharset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Judo Heroes - A Universal JavaScript demo application with React</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <div id="main"></div> <script src="/js/bundle.js"></script> </body> </html> 49
  • 50.
  • 51.
    .babelrc import webpack from'webpack'; import path from 'path'; const config = { entry: { js: './src/app-client.js' }, output: { path: path.join(__dirname, 'src', 'static', 'js'), filename: 'bundle.js' }, module: { loaders: [{ test: path.join(__dirname, 'src'), loaders: [{ loader: 'babel-loader' }] }] }, }; export default config; .webpack.config.babel.js { "presets": ["react", "es2015"] } 51
  • 52.
  • 53.
    // src/server.js import pathfrom 'path'; import { Server } from 'http'; import Express from 'express'; const app = new Express(); const server = new Server(app); // define the folder that will be used for static assets app.use(Express.static(path.join(__dirname, 'static'))); // start the server const port = process.env.PORT || 3000; const env = process.env.NODE_ENV || 'production'; server.listen(port, err => { if (err) { return console.error(err); } console.info(`Server running on http://localhost:${port} [${env}]`); }); Static Express server 53
  • 54.
  • 55.
    RECAP What we learnedso far 1. Define views combining React components 2. Add Routing using React Router 3. Compiling our frontend bundle with Babel and Webpack 4. Run the app with a static Express server 55
  • 56.
  • 57.
  • 58.
    // src/views/index.ejs <!DOCTYPE html> <html> <head> <metacharset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Judo Heroes - A Universal JavaScript demo application with React</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <div id="main"><%- markup -%></div> <script src="/js/bundle.js"></script> </body> </html> Converting static index.html into a template 58
  • 59.
  • 60.
    // ... // universalrouting and rendering app.get('*', (req, res) => { match( { routes, location: req.url }, (err, redirectLocation, renderProps) => { // in case of error display the error message if (err) { return res.status(500).send(err.message); } // in case of redirect propagate the redirect to the browser if (redirectLocation) { return res.redirect(302, redirectLocation.pathname + redirectLocation.search); } // generate the React markup for the current route let markup; if (renderProps) { // if the current route matched we have renderProps markup = renderToString(<RouterContext {...renderProps}/>); } else { // otherwise we can render a 404 page markup = renderToString(<NotFoundPage/>); res.status(404); } // render the index template with the embedded React markup return res.render('index', { markup }); } ); }); 60
  • 61.
  • 62.
    RECAP What we learnedso far 1. Create a Single Page Application with React and React Router 2. Add server side routing and rendering using React and React Router libraries in out Express app 62
  • 63.
    UNIVERSAL DATA RETRIEVAL api-proxy& async-props (COMPLETE CHAPTER in ) UNIVERSAL STATE MANAGEMENT Redux Node.js Design Patterns WHERE DO WE GO from here... Code: https://github.com/lmammino/judo-heroes-2 63
  • 64.
    THANKS! loige loige.colmammino (Special thanksto , , Aleksandar Čambas & )@cirpo @andreaman87 @quasi_modal 64