Rubén Cid Lara

"Es una aplicación que muestra una previsión diaria de la presencia de medusas en las playas de Ibiza"

Índice

  • Módulos que implica.
  • Objetivos de la aplicación.
  • Análisis de los requerimientos.
  • Definición de las tareas.
  • Como funciona?
  • Nuestra competencia.
  • Nuestro servicio.
  • Modo producción.

Módulos que

Implica

Módulos

  • Diseño de interfaces Web.
  • Desarrollo en entorno servidor.
  • Desarrollo en entorno cliente.

Objetivos de la aplicación.

  • Que nos muestre la información en tiempo real del estado de las playas de Ibiza.

  • Las banderas que existen en ese mismo instante, mediante una colaboración ciudadana, que se podría extender a la colaboración con la Cruz Roja de Ibiza.

  • La temperatura que hay en la isla.

  • La afluencia de personas.

  • Además de una pequeña red social donde se pueden subir fotos y compartirlas en las distintas redes sociales como Instagram, Twitter, Facebook…

Análisis de los requerimientos

Requerimientos de la app

  • Funcional
  • Sistema de login independiente de la plataforma.
  • Mapa con cambios en tiempo real.
  • Red social de fotos.
  • Single page (UX).
  • Intuitiva.
  • Segura.
  • Multiplataforma, (un servicio y responsive).

DEfinición de la tareas

Wireframes

Desarrollo de la lógica del servidor

api

Desarrollo de la aplicación web

Desarrollo de la aplicación móvil

Testing

Paso a producción

Costes

Monetarios

  • Alta como desarrollador en Google Play: 50€/pago único.
  • Alta como desarrollador en App Store: 87,97€/anuales.
  • Dominio ibizajelly.com: 9.90€/anuales.

 

Fijos: 50€

Anuales: 97.87€

 

Costes

Horarios (564.5h)

  • Definición del proyecto y gestión del proyecto: 10h
  • Crear wireframe (mobile y desktop): 5h
  • Desarrollo de la API: 10h
  • Desarrollo de la lógica de la aplicación web: 300h
  • Desarrollo de la lógica de la app: 200h
  • Maquetación: 30h
  • Test Web (Unit testing): 4h
  • Test App (Unit testing): 4h
  • Publicación Web: 30min
  • Publicación App: 1h
  • Mantenimiento: ??

 

Como funciona?

Accedemos a:

52.41.44.230

Registramos

Colabora

  • Playa libre, el baño está permitido, que las condiciones para bañarse, nadar o bucear son buenas. 

 

  • Playa peligrosa, se permite el baño con limitaciones.        

 

  • Esta bandera de playa indica la prohibición del baño.      

 

  • El significado es claramente el aviso de presencia de MEDUSAS en el mar.

Estados

Nuestra competencia

iMedJelly

Es una aplicación que está disponible para IOS, que nos proporciona:

  • Condiciones climáticas.
  • Olas y condición de las corrientes.
  • Medusas.

Nuestro servicio

¿De qué se compone?

REST
API

Text

  • Sistema de autenticación JWT.
  • Sistema CRUD de usuarios.
  • Sistema CRUD de playas.
  • Sistema CRUD de fotos.

Web

APP

Mobile
app

Nuestro servicio

¿Posibles soluciones?

CMS

LAMP (LINUX-apache-maysq-php)

MEAN(Mongodb-express-angualar-node)

Nuestro servicio

¿Con qué tecnologías se ha creado?

¡MEAN!

Node.js

Node.JS

A que se debe su RAPIDEZ?

El V8 compila el JavaScript a código máquina nativo.

Web

sockets

SAILS.js

Mongodb

[
    {
        "_id": ObjectId("4efa8d2b7d284dad101e4bc7"),
        "Last Name": "PELLERIN",
        "First Name": "Franck",
        "Age": 29,
        "Address": {
            "Street": "1 chemin des Loges",
            "City": "VERSAILLES"
        }
    },
    {
        "_id": ObjectId("4efa8d2b7d284dad101e4sd3"),
        "Last Name": "CID",
        "First Name": "RUBEN",
        "Age": 19,
        "Address": {
            "Street": "1 chemin des Loges",
            "City": "IBIZA"
        }
    }
]

Angular 2

WEBCOMponents

Ventajas

  • Shadow DOM, ocultación de una estructura compleja HTML en un tag.
  • Configuración declarativa mediante atributos.
  • Reusable.
  • Importación de nuevos componentes.
  • Elementos personalizados

Github

Youtube

Componente en Angular 2

map.component.html

Otras ayudas

  • Foundation 6 SASS
  • SASS
  • TYPESCRIPT
  • WEBPACK
  • MAPBOX
  • IONIC
  • Inkscape
  • CHROMIUM
  • FIREFOX
  • Git
  • Bitbuket

Nuestro servicio

Estructura de los componentes

Servidor

API

Cliente

Text

main.ts

import {bootstrap}    from '@angular/platform-browser-dynamic';
import {AppComponent} from './app/app.component';
import {HTTP_PROVIDERS} from "@angular/http";
import {FORM_PROVIDERS} from '@angular/common';
import {WebSocket} from  './app/services/websocket.service';
import {TRANSLATE_PROVIDERS} from 'ng2-translate/ng2-translate';
import { ROUTER_PROVIDERS } from '@angular/router-deprecated';
import {RestApi} from "./app/services/rest.service";
// Modo produccion
import {enableProdMode} from "@angular/core";
enableProdMode();

//Lanzamiento
bootstrap(
  AppComponent, [
    HTTP_PROVIDERS,
    FORM_PROVIDERS,
    ROUTER_PROVIDERS,
    WebSocket,
    RestApi,
    TRANSLATE_PROVIDERS
  ]
).catch(err => console.error(err));

app.component.ts

import {Component, OnInit} from '@angular/core';
import {RouteConfig, ROUTER_DIRECTIVES} from '@angular/router-deprecated';
import {TranslateService, TranslatePipe} from 'ng2-translate/ng2-translate';
import {MapBoxComponent} from './map';
import {HomeComponent} from './home';

@Component({
  selector: 'ibiza-jelly',
  template: require('./app.component.html'),
  directives: [ROUTER_DIRECTIVES],
  pipes: [TranslatePipe]
})
// Sistema de enrutamiento
@RouteConfig([
  {
    path: '/',
    name: 'Home',
    component: HomeComponent,
    useAsDefault: true
  },
  {
    path: '/map',
    name: 'Map',
    component: MapBoxComponent
  }
])
export class AppComponent {
  constructor(translate:TranslateService) {
    var userLang = navigator.language.split('-')[0]; // Idioma del navegador
    userLang = /(es|en)/gi.test(userLang) ? userLang : 'es';
    // Idioma por defecto
    translate.setDefaultLang('es');
    // Usamos el idioma del navegaor
    translate.use(userLang);
  }
}

app.component.html

Si vamos a '/'

renderiza Home

home.component.ts

import {Component, ElementRef, OnInit} from '@angular/core';
import {HeaderMainComponent} from './header';
import {ContentComponent} from './content';
import {FooterComponent} from './footer';
import {Router} from '@angular/router-deprecated';
import {tokenNotExpired} from "../shared/token.service";

@Component({
  selector: 'ibiza-jelly',
  template: require('./home.component.html'),
  directives: [HeaderMainComponent, ContentComponent, FooterComponent],
  styles: [require('./home.component.scss')]
})
export class HomeComponent implements OnInit {
  public token: string;
  notification: boolean = false;
  msgNoti: any;
  typeForm: string = 'signup';

  constructor(private router: Router, private ele: ElementRef) {}

  notify(e) {
    this.notification = true;
    this.msgNoti = e;
    let pro = new Promise((resolve) => {
      setTimeout(()=> {
        let ele = this.ele.nativeElement.querySelector('.notify');
        if (!!ele) {
          ele.classList.remove('bounceInLeft');
        }
        resolve(null);
      }, 5000);
    });
    pro.then(()=> {
      this.notification = false;
    });
  }

  ngOnInit() {
    if (tokenNotExpired('token')) {
      this.router.navigate(['Map']);
    }
  }

  changeFormType(e) {
    console.log(e + ' evento');
    this.typeForm = e;
  }
}

home.component.html

<ijelly-header (typeFormChanged)="changeFormType($event)" [typeForm]="typeForm"></ijelly-header>
<ijelly-content></ijelly-content>
<div  class="notify animated bounceInLeft" [ngClass]="{error: msgNoti.error}" *ngIf="notification">
  <span class="notify-text">{{msgNoti.msg}}</span>
</div>
<ijelly-footer (typeFormChanged)="changeType($event)"
               [typeForm]="typeForm"
               (notification)="notify($event)"></ijelly-footer>

header.component.ts

import {Component, Input, Output, EventEmitter} from '@angular/core';
import {ROUTER_DIRECTIVES} from '@angular/router-deprecated';
import {TranslateService, TranslatePipe} from 'ng2-translate/ng2-translate';

@Component({
  selector: 'ijelly-header',
  template: require('./header.component.html'),
  styles: [require('./header.component.scss')],
  directives: [ROUTER_DIRECTIVES],
  pipes: [TranslatePipe]
})
export class HeaderMainComponent {
  @Output() typeFormChanged = new EventEmitter();
  @Input() typeForm:string;

  constructor(private translate: TranslateService) {
  }

  setTypeForm(type) {
    console.log(type);
    this.typeForm = type;
    this.typeFormChanged.emit(this.typeForm);
  }
}

header.component.html

<header>
  <nav class="align-justify align-middle row">
    <div class="align-justify align-middle row">
      <ul>
        <li class="show-for-large"><a [routerLink]="['Home']">Home</a></li>
        <li class="show-for-large"><a data-scroll data-options='{ "easing": "easeInQuad" }' href="#form-login-register">{{'MAP' | translate}}</a></li>
      </ul>
      <ul>
        <li class="show-for-large"><a (click)="setTypeForm('signin')">{{'LOGIN' | translate}}</a></li>
        <li class="show-for-large"><a (click)="setTypeForm('signup')">{{'SIGNUP' | translate}}</a></li>
        <li class="hide-for-large"><a href="javascript:void(0)">
          <i class="fa fa-bars fa-2x" aria-hidden="true"></i>
        </a></li>
      </ul>
    </div>
  </nav>
</header>

footer.component.ts

import {Component, OnInit, Output, Input, EventEmitter} from '@angular/core';
import {ControlGroup, FormBuilder, Validators, Control} from '@angular/common';
import {CustomValidators} from '../../shared/email-validator';
import {RestApi} from '../../services/rest.service';
import {SignUpUser} from '../../models/rest.models';
import {TranslateService, TranslatePipe} from 'ng2-translate/ng2-translate';
import {ROUTER_DIRECTIVES, Router} from '@angular/router-deprecated';
import {LocalStorage} from "../../services/localstorage.service";

@Component({
  selector: 'ijelly-footer',
  template: require('./footer.component.html'),
  styles: [require('./footer.component.scss')],
  providers: [RestApi],
  directives: [ROUTER_DIRECTIVES],
  pipes: [TranslatePipe]
})
export class FooterComponent extends LocalStorage implements OnInit {
  public registerForm:ControlGroup;
  public loginForm:ControlGroup;
  public username:Control;
  public email:Control;
  public password:Control;
  public emailLogin:Control;
  public notification;

  @Output() notification = new EventEmitter();
  @Output() typeFormChanged = new EventEmitter();
  @Input() typeForm:string;

  constructor(private _formBuilder:FormBuilder,
              private _api:RestApi,
              private translate:TranslateService,
              private _router:Router) {
    super();
  }

  createUser(user:SignUpUser) {
    this._api.createUser(user).subscribe(() => {
      this.notification.emit({msg: 'El usuario se ha creado correctamente', error: false});
      // this.changeType('signin');
    }, () => {
      this.notification.emit({msg: 'No se ha podido crear el usuario', error: true});
    });
  }

  loginUser(user) {
    this._api.loginUser(user).subscribe(
      (infoUser) => {
        if (infoUser && infoUser.json()) {
          this.setId(infoUser.json().id);
          this._api.getJWToken().subscribe(
            (token) => {
              token = token.json().token;
              this._api.setToken(token);
              this._router.navigate(['Map']);
            }
          );
          return;
        }
        this.notification.emit({msg: "Email o contraseña inválida.", error: true});
      },
      (error) => {
        this.notification.emit({msg: error.json().error, error: true});
      }
    );
  }

  ngOnInit():any {
    this.username = new Control('', Validators.compose([Validators.required]),
      CustomValidators.checkUsername
    );
    this.email = new Control('', Validators.compose([Validators.required,
      CustomValidators.emailValidator]),
      CustomValidators.checkEmail
    );
    this.password = new Control('', Validators.compose([Validators.required,
      Validators.minLength(8)]));
    this.emailLogin = new Control('', Validators.compose([Validators.required,
      CustomValidators.emailValidator]),
      CustomValidators.checkEmailLogin);
    this.registerForm = this._formBuilder.group({
      'username': this.username,
      'email': this.email,
      'password': this.password
    });
    this.loginForm = this._formBuilder.group({
      'email': this.emailLogin,
      'password': this.password
    });
  }

  changeType(type) {
    this.typeForm = type;
    this.typeFormChanged.emit(type);
  }
}

footer.component.html

<footer>
  <div class="main" [ngSwitch]='typeForm' id="form-login-register">
    <!--swicth-->
    <div class="center" *ngSwitchWhen="'signup'">
      <h2 >{{'BEGINJOY' | translate}}</h2>
      <h3 class="text-center">{{'BEGINJOYTEXT' | translate}}</h3>
      <form action=""  novalidate [ngFormModel]="registerForm" (ngSubmit)="createUser(registerForm.value)">
        <label>{{'USERNAME' | translate}}:
          <input
            [class.error]="username.errors && username.dirty && !username.valid && !username.pending"
            [class.correct]="!username.errors && username.dirty && username.valid && !username.pending"
            type="text" name="username" ngControl="username">
          <div *ngIf="username.dirty && !username.valid && !username.pending">
            <small class="error" *ngIf="username.errors && username.errors.usernameTaken">*{{ "EXISTS" | translate}}</small>
          </div>
          <div *ngIf="username.dirty && username.valid && !username.pending">
            <small class="correct" *ngIf="!username.errors">Ok</small>
          </div>
        </label>
        <label>Email:
          <input
            [class.error]="email.errors && email.dirty && !email.valid && !email.pending"
            [class.correct]="!email.errors && email.dirty && email.valid && !email.pending"
            type="email" name="email" ngControl="email">
          <div *ngIf="email.dirty && !email.valid && !email.pending">
            <small class="error" *ngIf="email.errors && email.errors.emailTaken">*{{ "EXISTS" | translate}}</small>
          </div>
          <div *ngIf="email.dirty && email.valid && !email.pending">
            <small class="correct" *ngIf="!email.errors">Ok</small>
          </div>
        </label>
        <label>{{'PASSWORD' | translate}}:
          <input type="password"
                 [class.error]="password.errors && password.dirty && !password.valid && !password.pending"
                 [class.correct]="!password.errors && password.dirty && password.valid && !password.pending"
                 name="password" ngControl="password" >
          <div *ngIf="password.dirty && !password.valid && !password.pending">
            <small class="error" *ngIf="password.errors">*{{'MINIMUM8' | translate}}</small>
          </div>
          <div *ngIf="password.dirty && password.valid && !password.pending">
            <small class="correct" *ngIf="!password.errors">Ok</small>
          </div>
        </label>
        <button class="button primary"  type="submit" [disabled]="!registerForm.valid">{{'CREATEACCOUNT' | translate}}</button>
      </form>
      <a (click)="typeForm = 'signin'">{{'LOGIN' | translate}}</a>
    </div>

    <div class="center" *ngSwitchWhen="'signin'">
      <h2 >{{'BEGINJOY' | translate}}</h2>
      <h3 class="text-center">{{'BEGINJOYTEXT' | translate}}</h3>
      <form action=""  novalidate [ngFormModel]="loginForm" (ngSubmit)="loginUser(loginForm.value)">
        <label>Email:
          <input
            [class.error]="emailLogin.errors && emailLogin.dirty && !emailLogin.valid && !emailLogin.pending"
            [class.correct]="!emailLogin.errors && emailLogin.dirty && emailLogin.valid && !emailLogin.pending"
            type="email" name="email" ngControl="email">
          <div *ngIf="emailLogin.dirty && !emailLogin.valid && !emailLogin.pending">
            <small class="error" *ngIf="emailLogin.errors">*{{ "NOEXISTS" | translate}}</small>
          </div>
          <div *ngIf="emailLogin.dirty && emailLogin.valid && !emailLogin.pending">
            <small class="correct" *ngIf="!emailLogin.errors">Ok</small>
          </div>
        </label>
        <label>{{'PASSWORD' | translate}}:
          <input type="password"
                 [class.error]="password.errors && password.dirty && !password.valid && !password.pending"
                 [class.correct]="!password.errors && password.dirty && password.valid && !password.pending"
                 name="password" ngControl="password" >
          <div *ngIf="password.dirty && !password.valid && !password.pending">
            <small class="error" *ngIf="password.errors">*{{'MINIMUM8' | translate}}</small>
          </div>
          <div *ngIf="password.dirty && password.valid && !password.pending">
            <small class="correct" *ngIf="!password.errors">Ok</small>
          </div>
        </label>
        <button class="button primary"  type="submit" [disabled]="!loginForm.valid">{{'LOGIN' | translate}}</button>
      </form>
      <a (click)="typeForm = 'signup'">{{'SIGNUP' | translate}}</a>
    </div>
  </div>
</footer>

Validadores

import {Http, HTTP_PROVIDERS} from '@angular/http';
import {ReflectiveInjector} from '@angular/core';
import {Control} from '@angular/common';

function checkUser(control: Control, source: string, register: boolean = true) {

  // Inyeccion manual Http
  let injector = ReflectiveInjector.resolveAndCreate([HTTP_PROVIDERS]);
  let http = injector.get(Http);

  let body = JSON.stringify({[source]: control.value});

  return new Promise(resolve => {
    http.post('/user/check', body)
      .subscribe(value => {
        let msg = value.json().message;
        let rt = null;
        if (!register) {
          rt = {[msg]: true};
        }
        resolve(rt);
      }, error => {
        let msg = error.json().message;
        let rt = null;
        if (register) {
          rt = {[msg]: true};
        }
        resolve(rt);
      });
  });
}
export class CustomValidators {
  static emailValidator(control: Control) {
    let email = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
    if (!control.value.match(email)) {
      return {'invalidEmailAddress': true};
    }
  }

  static checkUsername(control: Control) {
    return checkUser(control, 'username');
  }

  static checkEmail(control: Control) {
    return checkUser(control, 'email');
  }

  static checkEmailLogin(control: Control) {
    return checkUser(control, 'email', false);
  }
}

Check bakend

check: function (req, res) {
    var find;
    var email = false;
    if(!!req.param('username')){
      find = {username: req.param('username')};
    }else if(!!req.param('email')){
      find = {email: req.param('email')};
      email = true;
    }
    if(!!find){
      User.findOne(find).exec((err, user) => {
        if(err) return res.negotiate(err);
        if(!!user && !email){
          // nombre de usuario ya esta en uso
          res.status(400).json({'message': 'usernameTaken'});
          return;
        }else if(!!user && !!email){
          // mail ya esta en uso
          res.status(400).json({'message': 'emailTaken'});
          return;
        }
        // no esta cogido
        res.json({'message':  null});
        return;
      });
      return;
    }
    res.json({'message':  null});
    return;
 
  }

TODA LA APLICACIóN EN UN TAG HTML!

<!DOCTYPE html>
<html>
  <head>
    <base href="/">
    <title><%=typeof title == 'undefined' ? 'IbizaJelly' : title%></title>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">

    <!--STYLES-->
    <link rel="stylesheet" href="/styles/font-awesome.min.css">
    <link rel="stylesheet" href="/styles/importer.css">
    <link rel="stylesheet" href="/styles/normalize.css">
    <!--STYLES END-->

  </head>

  <body>

    <ibiza-jelly></ibiza-jelly>
 


    <!--SCRIPTS-->
    <script src="/js/dependencies/sails.io.js"></script>
    <script src="/js/dependencies/modernizr.custom.js"></script>
    <script src="/js/dependencies/pathLoader.js"></script>
    <script src="/js/dependencies/smooth-scroll.min.js"></script>
    <script src="/js/dependencies/upup.min.js"></script>
    <script src="/js/polyfills.js"></script>
    <script src="/js/vendor.js"></script>
    <script src="/js/app.js"></script>
    <script src="/js/classie.js"></script>
    <script src="/js/main.js"></script>
    <!--SCRIPTS END-->

    <script>
      smoothScroll.init({
        speed: 500, // Integer. How fast to complete the scroll in milliseconds
        easing: 'easeInOutCubic', // Easing pattern to use
        updateURL: false // Boolean. If true, update the URL hash on scroll
      });
    </script>
  </body>
</html>

Modo producción en marcha

AMAZON

EC2

PaaS

Las características que nos ofrece el servicio gratuito de Amazon son:

 

  • 750 horas de uso de EC2 con una instancia t2.micro de Linux, RHEL, o SLES

  • Se añaden 15 GB de ancho de banda saliente en todos los servicios de AWS

  • 1 GB de transferencia de datos regionales.

  • Docker.

Deploy

Continuará...

  • Compartir fotos en la aplicación.
  • Integración con redes sociales.
  • Modificación del estado de las playas sólo cuando este a menos de 300m.
  • Usuario Premium. Calas escondidas, eventos en la isla...
  • Mapa SVG.
  • Offline.
  • Sistema de logs.
  • Descripción basada Tour basado en el scroll.

Preguntas?

Made with Slides.com