
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?
IbizaJelly
By Rubén Cid Lara
IbizaJelly
- 1,099