Framework Angular

Angular – Aplicación para el clima con API

Clima con Angular – App que consume API

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:

  • Ubicación del usuario con nombre de la ciudad y país
  • Reloj
  • Reporte del clima para los próximos 5 días usando imágenes
  • La temperatura máxima y mínima esperada para determinado día

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.

Sobre la API para el clima

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

Servicio para ubicación y clima

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.

Componente de detalle de clima

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.

Componente de app principal

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:

Pantalla de carga de la app

App responsiva

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:

Angular – App de clima responsiva

Demostración y explicación en vídeo

He grabado un vídeo en YouTube. Puede que ahí se resuelvan algunas de tus dudas o entiendas de mejor manera:

Conclusión y descargas

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.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

3 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

3 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

3 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

3 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

3 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

4 días hace

Esta web usa cookies.