Hoy te voy a mostrar un sistema web hecho con Angular. Se trata de una app web que muestra el clima a través de una API.
Lo que hace este software es obtener la ubicación del usuario a través de su IP y obtener el pronóstico del tiempo usando otra API a partir de la latitud y longitud obtenidas anteriormente.
Verás que está implementado de una manera muy sencilla utilizando componentes y servicios. Al final vamos a tener una app web que muestra:
Además, la aplicación web será responsiva pues vamos a usar Bootstrap. Al final del post dejaré el enlace del repositorio para que puedas explorar el código fuente y descargarlo si es necesario. Como lo dije, utiliza Angular.
Vamos a usar la API de 7timer. Decidí utilizarla porque no requiere una clave API y solo necesita la latitud y longitud. El endpoint es:
http://www.7timer.info/bin/civillight.php?lon=[LONGITUD]&lat=[LATITUD]&ac=0&unit=metric&output=json
La respuesta de la API es parecida a lo siguiente, en donde vemos que devuelve el clima en dataseries:
{
"product": "civillight",
"init": "2020062718",
"dataseries": [
{
"date": 20200628,
"weather": "ishower",
"temp2m": {
"max": 32,
"min": 22
},
"wind10m_max": 2
},
{
"date": 20200629,
"weather": "rain",
"temp2m": {
"max": 27,
"min": 22
},
"wind10m_max": 2
},
{
"date": 20200630,
"weather": "rain",
"temp2m": {
"max": 30,
"min": 21
},
"wind10m_max": 2
},
{
"date": 20200701,
"weather": "cloudy",
"temp2m": {
"max": 31,
"min": 22
},
"wind10m_max": 2
},
{
"date": 20200702,
"weather": "rain",
"temp2m": {
"max": 32,
"min": 22
},
"wind10m_max": 2
},
{
"date": 20200703,
"weather": "cloudy",
"temp2m": {
"max": 34,
"min": 21
},
"wind10m_max": 2
},
{
"date": 20200704,
"weather": "ts",
"temp2m": {
"max": 35,
"min": 22
},
"wind10m_max": 2
}
]
}
Cada objeto de dataseries
tiene datos interesantes. En weather
tenemos el clima (soleado, nublado, lluvia, solo que en inglés según los valores de la API).
Dentro de date
tenemos la fecha, mientras que en temp2m
está la temperatura máxima y mínima. También contamos con el viento, pero en este caso no usaremos ese valor.
Nota: en este caso vamos a obtener la ubicación a través de la IP, pero eres libre de obtenerla por otros medios, por ejemplo, usando el GPS para mayor precisión.
Nota 2: eres libre de explorar la documentación de la API, está en http://7timer.info/doc.php?lang=en#machine_readable_api
Pasemos a la parte del código. Tenemos las llamadas a ambas APIs con Angular usando el lenguaje TypeScript:
/*
Programado por Luis Cabrera Benito
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
Blog: https://parzibyte.me/blog
Ayuda: https://parzibyte.me/blog/contrataciones-ayuda/
Contacto: https://parzibyte.me/blog/contacto/
*/import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ClimaService {
private RUTA_API_UBICACION = "https://freegeoip.app/json/";
constructor() { }
async obtenerDatosUbicacion() {
const response = await fetch(this.RUTA_API_UBICACION);
return await response.json();
}
async obtenerDatosDeClima(longitude: string, latitude: string) {
const response = await fetch(`http://www.7timer.info/bin/civillight.php?lon=${longitude}&lat=${latitude}&ac=0&unit=metric&output=json`);
return await response.json();
}
parsearFecha(value) {
value = "" + value;
if (!value) {
return "";
}
let anio = value.substring(0, 4);
let mes = value.substring(4, 6);
let dia = value.substring(6, 8);
return anio + "-" + mes + "-" + dia;
}
}
Igualmente he incluido una función que parsea o convierte la fecha. Sé que debería colocarla en otro lugar pero era la única función que quedaba sin lugar. Lo que hace es agregar guiones a la fecha.
En la app reutilizo el componente de detalle de clima, para evitar la repetición de código. El diseño queda así:
<!--
Programado por Luis Cabrera Benito
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
Blog: https://parzibyte.me/blog
Ayuda: https://parzibyte.me/blog/contrataciones-ayuda/
Contacto: https://parzibyte.me/blog/contacto/
-->
<div class="card">
<div class="card-body">
<div class="row text-center">
<div class="col-lg-4 col-12">
<h1 class="display-1">{{detalles.temp2m.max}}°</h1>
<p>Mínima: {{detalles.temp2m.min}}°</p>
</div>
<div class="col-lg-4 col-12"><img style="max-width: 200px;" class="img-fluid mb-2" src="{{resolverImagen()}}" alt="Imagen del clima">
</div>
<div class="col-lg-4 col-12">
<p class="h1">
{{detalles.date | fechaANombreDia}}
</p>
<p class="h6">
{{detalles.date | formatearFecha}}
</p>
</div>
</div>
</div>
</div>
Aquí hay un detalle importante y es que dentro de la carpeta assets tengo todas las imágenes. El nombre de la imagen representa el clima, y el mismo es dado por la función resolverImagen
que vemos en la funcionalidad del componente:
/*
Programado por Luis Cabrera Benito
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
Blog: https://parzibyte.me/blog
Ayuda: https://parzibyte.me/blog/contrataciones-ayuda/
Contacto: https://parzibyte.me/blog/contacto/
*/import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-weather-detail',
templateUrl: './weather-detail.component.html',
styleUrls: ['./weather-detail.component.css']
})
export class WeatherDetailComponent implements OnInit {
@Input() detalles: any
constructor() { }
resolverImagen() {
return `assets/${this.detalles.weather}.png`;
}
ngOnInit(): void {
}
}
Dentro de la propiedad detalles (que son los datos que pasa el componente padre) accedemos a weather
, y como lo dije, este nombre está relacionado con el nombre de las imágenes.
Quiero aclarar esa parte de las imágenes porque no he colocado todas: así que si al probar hay un clima que no tiene imagen, simplemente verifica en la consola cuál imagen dio error 404 y agrega una con ese nombre.
Ahora que ya vimos el componente de detalle de clima, veamos el componente principal. Este componente se encarga de invocar al servicio para obtener los datos del clima. Luego, divide los resultados para mostrar el de hoy y los restantes.
Comenzamos viendo el diseño. Mostramos los detalles de la ubicación, un reloj y luego mostramos los detalles del clima. En este caso usamos dos veces el componente de detalle de clima.
Primero lo usamos para mostrar el clima de hoy, y luego hacemos un ngFor para mostrarlos en una tarjeta de manera repetitiva:
<!--
Programado por Luis Cabrera Benito
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
Blog: https://parzibyte.me/blog
Ayuda: https://parzibyte.me/blog/contrataciones-ayuda/
Contacto: https://parzibyte.me/blog/contacto/
-->
<main class="container-fluid">
<div class="row" *ngIf="cargando">
<div class="col-12 text-center">
<h1 class="display-1">Cargando. Espere un momento por favor</h1>
<img src="assets/25.gif" alt="">
</div>
</div>
<div *ngIf="!cargando" class="row">
<div class="col-12">
<div class="text-center mt-2" *ngIf="!cargando">
<strong class="display-4">{{city}}, {{region_name}}, {{country_name}}<br> {{hora}}</strong>
</div>
</div>
<div class="col-12">
<app-weather-detail [detalles]="detallesHoy"></app-weather-detail>
</div>
<div *ngFor="let detalles of detallesProximos;" class="col-12 col-md-6 my-2">
<app-weather-detail [detalles]="detalles"></app-weather-detail>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card border-success mb-3">
<div class="card-header">Créditos</div>
<div class="card-body text-success">
<pre>
Programado por Luis Cabrera Benito
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
Blog: https://parzibyte.me/blog
Ayuda: https://parzibyte.me/blog/contrataciones-ayuda/
Contacto: https://parzibyte.me/blog/contacto/
</pre>
<a href="https://parzibyte.me/blog/contrataciones-ayuda/" class="btn btn-warning btn-lg">Ayuda y soporte</a>
<h1>Imágenes</h1>
cloudy: Icons made by <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a> from <a
href="https://www.flaticon.com/" title="Flaticon"> www.flaticon.com</a>
<br>
clear: Icons made by <a href="https://www.flaticon.com/authors/good-ware" title="Good Ware">Good
Ware</a>
from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
<br>
ishower,oshower: Icons made by <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a>
from <a href="https://www.flaticon.com/" title="Flaticon"> www.flaticon.com</a>
<br>
mcloudy: Icons made by <a href="https://www.flaticon.com/authors/srip" title="srip">srip</a> from <a
href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
<br>
rain: Icons made by <a href="https://www.flaticon.com/authors/dinosoftlabs"
title="DinosoftLabs">DinosoftLabs</a>
from <a href="https://www.flaticon.com/" title="Flaticon"> www.flaticon.com</a>
<br>
snow: Icons made by <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a> from <a
href="https://www.flaticon.com/" title="Flaticon"> www.flaticon.com</a>
<br>
tsrain: Icons made by <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a> from <a
href="https://www.flaticon.com/" title="Flaticon"> www.flaticon.com</a>
<br>
ts: Icons made by <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a> from <a
href="https://www.flaticon.com/" title="Flaticon"> www.flaticon.com</a>
<br>
</div>
</div>
</div>
</div>
</main>
Dentro del funcionamiento del componente principal tenemos a las funciones que es en donde se hace el verdadero consumo de la API:
/*
Programado por Luis Cabrera Benito
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
Blog: https://parzibyte.me/blog
Ayuda: https://parzibyte.me/blog/contrataciones-ayuda/
Contacto: https://parzibyte.me/blog/contacto/
*/import { Component, OnInit } from '@angular/core';
import { ClimaService } from './weather.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
cargando = false;
city = "";
region_name = "";
country_name = "";
hora = "";
detallesHoy = {};
detallesProximos = [];
constructor(private weatherService: ClimaService) {
}
comenzarReloj() {
const _this = this;
setInterval(() => {
let hora = "";
let fecha = new Date();
let horas = fecha.getHours();
let minutos = fecha.getMinutes();
let segundos = fecha.getSeconds();
let horasArregladas = horas.toString();
if (horas < 10) {
horasArregladas = "0" + horasArregladas;
}
let minutosArreglados = minutos.toString();
if (minutos < 10) {
minutosArreglados = "0" + minutosArreglados;
}
let segundosArreglados = segundos.toString();
if (segundos < 10) {
segundosArreglados = "0" + segundosArreglados;
}
_this.hora = `${horasArregladas}:${minutosArreglados}:${segundosArreglados}`;
}, 500);
}
async ngOnInit() {
// Hacer que se muestre el indicador de carga
this.cargando = true;
// Obtener los datos de ubicación
const datosDeUbicacion = await this.weatherService.obtenerDatosUbicacion();
this.city = datosDeUbicacion.city;
this.region_name = datosDeUbicacion.region_name;
this.country_name = datosDeUbicacion.country_name;
const { latitude, longitude } = datosDeUbicacion;
// Obtener, ahora, los datos del clima
const datosDeClima = await this.weatherService.obtenerDatosDeClima(latitude, longitude);
// Cortamos el arreglo para mostrar la de hoy, y también las siguientes
this.detallesHoy = datosDeClima.dataseries.slice(0, 1)[0];
this.detallesProximos = datosDeClima.dataseries.slice(1, 5);
// Ocultamos el indicador de carga y comenzamos el reloj
this.cargando = false;
this.comenzarReloj();
}
}
Por cierto, para el reloj solo basta con refrescar la hora cada medio milisegundo usando setInterval. También se cuenta con un indicador de carga que se ve así, y hace que la app muestre retroalimentación al usuario:
También quiero mostrar que las tarjetas del clima se adaptan a la pantalla. Por ejemplo, así se ve en una pantalla de móvil:
He grabado un vídeo en YouTube. Puede que ahí se resuelvan algunas de tus dudas o entiendas de mejor manera:
Así es como queda esta app de consumo de clima con Angular. Puedes tomarla como base para tu proyecto, aprender un poco, o lo que tú quieras.
Recuerda que está creada con Angular así que puedes ejecutar su versión para producción con ng build
; agregar más componentes, etcétera.
El código fuente lo dejo en un repositorio de mi cuenta de GitHub, ahí puedes explorarlo y clonarlo.
En caso de que descargues el código, debes contar con la Angular CLI así como con npm
. Después de eso, solo es cuestión de ejecutar npm install
, esperar a que las dependencias sean instaladas y finalmente ejecutar ng serve
para visitar localhost:4200
Si te gusta este framework, te invito a ver más proyectos con Angular.
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…
Esta web usa cookies.