Sesion 5 Javascript II - jesusgpa/2023-2024-CSAAI GitHub Wiki

Sesión 5: Javascript II

  • Tiempo: 2h (50 + 50 min)
  • Fecha: Martes 27 de Febrero de 2024
  • Objetivos de la sesión:
    • Seguimos aprendiendo conceptos básicos de Javascript, que nos permitan hacer las prácticas
    • Manejando variables
    • Trabajando con objetos
    • Temporizaciones
    • Arrays
    • Agrupando elementos

Contenido

Introducción

Aprenderemos conceptos de javascript mediante ejemplos que iremos poco a poco ampliando

Contando clics

Vamos a hacer un programa en javascript que lleve la cuenta de las veces que hacemos clic sobre un botón.

Ejemplo 1: Contador de pulsaciones de un botón

En el fichero HTML tenemos un párrafo que hace las veces de display, y un botón para incrementar el contador

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="Ej-01.js" defer></script>
    <title>Ejemplo 1</title>
</head>
<body>
    <p id="display">0</p>
    <button type="button" id="boton">INC</button>
</body>
</html>

Este es el fichero javascript que arrancará en cuanto el DOM esté listo, gracias al atributo defer

//-- Contador de clics de botón

console.log("Ejecutando JS...");

//-- Acceder a los elementos del DOM
const display = document.getElementById("display");
const boton = document.getElementById("boton");

//-- Contador de clics
let cont = 0;

//-- Configurar retrollamada del boton
boton.onclick = () => {
  console.log("Clic!");

  //-- Incrementar contador
  cont += 1;

  //-- Actualizar el display
  display.innerHTML = cont;
}

Primero obtenemos todos los elementos de la interfaz gráfica que necesitamos: el display y el pulsador.

Luego definimos una variable contador, inicializada a 0.

Finalmente modificamos la función de retrollamada del botón para que cada vez que se apriete se incremente el contador y se introduzca su valor en el display.

Lo lanzamos en el navegador, y tras darle cuatro veces al pulsador tendríamos lo siguiente:

Y en esta animación lo vemos en acción:

Cambiando de color

Este ejemplo es similar al anterior, y lo que hacemos es cambiar el color del div al pulsar el botón.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Cambiar Color</title>

  <link rel="stylesheet" href="color-change.css">

  <script src="color-change.js" defer></script>
</head>
<body>
  <div id="elemento"></div>
  <button id="boton">Cambiar Color</button>
</body>
</html>
#elemento { 
    width: 100px; 
    height: 100px; 
    background-color: red; 
}
console.log("Ejecutando JS...");

const elemento = document.getElementById("elemento");
const boton = document.getElementById("boton");

boton.onclick = () => {
  console.log("Clic!");

  //-- Cambiar color
  elemento.style.backgroundColor = "blue";
}

Ahora me ves ...

En este ejemplo, mostramos y ocultamos un elemento haciendo clic en el botón.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mostrar/Ocultar</title>

  <link rel="stylesheet" href="hide-show.css">

  <script src="hide-show.js" defer></script>
</head>
<body>
  <div id="elemento"></div>
  <button id="boton">Mostrar/Ocultar</button>
</body>
</html>
#elemento {
    width: 100px; 
    height: 100px; 
    background-color: red; 
    display: none;
}
console.log("Ejecutando JS...");

const elemento = document.getElementById("elemento");
const boton = document.getElementById("boton");

let visible = false;

boton.onclick = () => {
  console.log("Clic!");

  //-- Alternar visibilidad
  if (visible) {
    elemento.style.display = "none";
  } else {
    elemento.style.display = "block";
  }
  visible = !visible;
}

Validar formulario

En este ejemplo, vemos cómo validar la información que ha introducido el usuario antes de enviar al servidor.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Validación de Formulario</title>

  <script src="validate-form.js" defer></script>

</head>
<body>
  <form id="formulario" action="/ruta/del/servidor" method="post">
    <input type="text" id="nombre" placeholder="Nombre">
    <input type="email" id="email" placeholder="Correo electrónico">
    <button id="boton">Enviar</button>
  </form>
</body>
</html>
console.log("Ejecutando JS...");

const formulario = document.getElementById("formulario");
const boton = document.getElementById("boton");

boton.onclick = (event) => {
  event.preventDefault(); // Evitar el envío del formulario

  const nombre = document.getElementById("nombre").value;
  const email = document.getElementById("email").value;

  //-- Validar nombre y email
  if (nombre.trim() === "" || email.trim() === "") {
    alert("Por favor, complete todos los campos.");
  } else {
    formulario.submit(); // Enviar el formulario
    formulario.reset(); // Reiniciar formulario
    alert("Formulario enviado correctamente.");    
  }
}

Ejemplo 2: Añadiendo los clics al párrafo

Haremos lo mismo que antes: un contador de clics, pero ahora en vez de meter el nuevo valor machacando el que había antes, lo añadiremos al párrafo a continuación del anterior.

Basta con usar el operador de añadir cadenas: +=

//-- Contador de clics de botón

console.log("Ejecutando JS...");

//-- Acceder a los elementos del DOM
const display = document.getElementById("display");
const boton = document.getElementById("boton");

//-- Contador de clics
let cont = 0;

//-- Configurar retrollamada del boton
boton.onclick = () => {
  console.log("Clic!");

  //-- Incrementar contador
  cont += 1;

  //-- Actualizar el display
  display.innerHTML += ' ' + cont;
}

Lo que veremos en el párrafo es similar a esto.

Cada vez que hacemos clic el contador se incrementa y se añade al párrafo, por lo que va creciendo.

En esta animación lo vemos en acción:

Trabajando con objetos

Los objetos en javascript son contenedores que almacenan pares nombre:valor.

Se puede tratar bien de datos, denominados atributos, o bien de acciones (funciones) que se denominan métodos.

Los objetos los usamos para organizar datos, agrupar variables y estructurar nuestro código para que sea más fácilmente reutilizable.

Se pueden crear de tres maneras:

  • Directamente al crear una variable, utilizando las llaves {}. Se denominan objetos literales
  • Mediante una función constructora
  • Mediante una clase

Objetos literales

Los objetos literales se crean al definir variables (let) o constantes (const).

Entre llaves {} se sitúan los pares nombre:valor.

La sintaxis genérica es la siguiente:

{
  nombre1: valor,
  nombre2: valor2,
  nombre3: valor3,
  ...
}

Donde los valores puede ser variables, constantes, otros objetos o funciones.

Las funciones situadas dentro de un objeto se denominan métodos.

Ejemplo 3: Contador con objeto literal

Vamos a reestructurar el Ejemplo 1 para usar objetos.

Antes usábamos la variable cont para almacenar el contador.

Ahora lo rehacemos de la siguiente manera:

  • Por un lado, agrupamos los dos elementos de la interfaz en el objeto gui.

A través de gui tenemos acceso a todo lo relacionado con la interfaz gráfica, en vez que tenerlo disperso en elementos aislados.

Esto es muy cómodo porque el objeto gui lo podemos pasar como parámetro a otras funciones.

  • Por otro lado, el elemento contador lo convertimos en un objeto que contiene por un lado su valor y por otro el método inc para incrementarlo.

El fichero HTML sigue siendo el mismo que antes:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="Ej-03.js" defer></script>
    <title>Ejemplo 3</title>
</head>
<body>
    <p id="display">0</p>
    <button type="button" id="boton">INC</button>
</body>
</html>

y este es el fichero javascript:

console.log("Ejecutando JS...");

//-- Crear objeto gui, con los elementos de la interfaz gráfica
//-- Al tenerlo agrupado podemos pasarlo como parámetro o asignarlo
//-- a otro objeto
const gui = {
  display: document.getElementById("display"),
  boton: document.getElementById("boton"),
}

//-- Objeto contador: Contiene el valor y el método para incrementarse
const counter = {
  valor: 0,
  inc : function(value) {
    this.valor += value;
    gui.display.innerHTML = this.valor;
  }
}

//-- Acciones: Ligar el botón al contador
gui.boton.onclick = () => {
  counter.inc(1)
}

La función de retrollamada del botón ahora sólo tiene que llamar al método counter.inc(1) para incrementarlo.

Hemos añadido el parámetro para poder hacer, en el siguiente ejemplo, que también se decrementa, sin más que cambiar el parámetro por -1.

El funcionamiento es exactamente igual que el del ejemplo anterior.

Las funciones en javascript se tratan como cualquier otro objeto, y por tanto se pueden usar para meterlas dentro de otros objeto, en un array, pasarlo como parámetro a otra función, etc...

Ejemplo 4: Contador de incremento/decremento

Ahora queremos añadir un nuevo botón para decrementar el contador cada vez que se pulse.

Por un lado modificamos el HTML para añadir el nuevo elemento:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="Ej-04.js" defer></script>
    <title>Ejemplo 4</title>
</head>
<body>
    <p id="display">0</p>
    <button type="button" id="boton_inc">INC</button>
    <button type="button" id="boton_dec">DEC</button>
</body>
</html>

En el código javascript añadimos el nuevo elemento dentro del objeto gui y establecemos una nueva función de retrollamada que invocará a counter.inc(-1).

El objeto contador queda exactamente igual: lo hemos podido reutilizar.

console.log("Ejecutando JS...");

//-- Crear objeto gui, con los elementos de la interfaz gráfica
//-- Al tenerlo agrupado podemos pasarlo como parámetro o asignárselo
//-- a otro objeto
const gui = {
  display: document.getElementById("display"),
  boton_inc: document.getElementById("boton_inc"),
  boton_dec: document.getElementById("boton_dec")
}

//-- Objeto contador: Contiene el valor y el método para incrementarse
const counter = {
  valor: 0,
  inc : function(value) {
    this.valor += value;
    gui.display.innerHTML = this.valor;
  }
}

//-- Acciones:
//-- Incrementar contador
gui.boton_inc.onclick = () => {
  counter.inc(1);
}

//-- Decrementar contador
gui.boton_dec.onclick = () =>{
  counter.inc(-1);
}

En la interfaz aparecen ahora los dos botones:

Y el nuevo funcionamiento se muestra en esta animación: incrementamos y decrementamos el contador

Constructores

Los objetos literales son muy cómodos, pero son únicos.

Sólo hay uno por cada declaración.

En muchas ocasiones necesitaremos utilizar más copias del mismo objeto.

O incluso crear los objetos bajo demanda, según los vayamos necesitando.

Ejemplo 5: Dos contadores con incremento/decremento

Modificaremos el ejemplo anterior para incluir dos contadores, cada uno controlado con sus propias teclas de incremento/decremento.

Para ello hay que definir el constructor del contador y luego crear los objetos a través de ese constructor, usando la palabra reservada new.

El fichero HTML es:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="Ej-05.js" defer></script>
    <title>Ejemplo 5</title>
</head>
<body>
    <p id="display1">0</p>
    <button type="button" id="boton_inc1">INC</button>
    <button type="button" id="boton_dec1">DEC</button>
    
    <p id="display2">0</p>
    <button type="button" id="boton_inc2">INC</button>
    <button type="button" id="boton_dec2">DEC</button>
</body>
</html>

Y este es el nuevo javascript.

La función counter es el constructor.

Devuelve un objeto contador.

Como argumento se le pasa el display a usar para cada contador creado.

Se almacena en el interior del objeto y se usa cada vez que se llama al método inc para modificar su valorjs

console.log("Ejecutando JS...");

//-- Crear objeto gui, con los elementos de la interfaz gráfica
//-- Al tenerlo agrupado podemos pasarlo como parámetro o asignarlo
//-- a otro objeto
const gui = {

  //-- Elementos gui del contador 1
  display1: document.getElementById("display1"),
  boton_inc1: document.getElementById("boton_inc1"),
  boton_dec1: document.getElementById("boton_dec1"),

  //-- Elementos gui del contador 2
  display2: document.getElementById("display2"),
  boton_inc2: document.getElementById("boton_inc2"),
  boton_dec2: document.getElementById("boton_dec2"),
}

//-- Constructor del objeto contador
//-- Se le pasa como parámetro su display
function counter(display)
{
  this.valor = 0;

  //-- Almacenar su display
  this.display = display;

  //-- Actualizar el contador
  //-- y mostrarlo en el display
  this.inc = (value) => {
    this.valor += value;
    this.display.innerHTML = this.valor;
  }
}

//-- Crear los dos objetos contadores
const c1 = new counter(gui.display1);
const c2 = new counter(gui.display2);

//-------- Accciones:
//-- Contador 1: Incrementar contador
gui.boton_inc1.onclick = () => {
  c1.inc(1);
}

//-- Decrementar contador
gui.boton_dec1.onclick = () =>{
  c1.inc(-1);
}

//-- Contador 2: Incrementar contador
gui.boton_inc2.onclick = () => {
  c2.inc(1);
}

//-- Decrementar contador
gui.boton_dec2.onclick = () =>{
  c2.inc(-1);
}

Esta es la pinta que tiene la interfaz

y en esta animación se muestra su funcionamiento

Tenemos dos contadores independientes, pero con su estructura definida en un constructor único.

Clases

A partir del 2015, se incluyó la posibilidad de utilizar clases, que nos permiten definir la estructura de los objetos, de forma similar a como se hace en otros lenguajes de programación.

La sintaxis de la clase es la siguiente:

class nombre_clase {
  
  //-- Constructor de la clase
  constructor() {
    //.....
    //... Definición de las propiedades
  }

  //-- Métodos
  metodo1() {
    //...
  }

  metodo2() {
    //...
  }

}

Ejemplo 6: Clase counter

Vamos a modificar el ejemplo 5 para utilizar la clase counter.

Sólo tenemos que sustituir la función constructora counter que teníamos antes por la nueva clase.

El resto del código es igual.

//-- Clase counter para crear contadores
//-- Hay que pasarle como parámetro en el constructor  
//-- el display usado
class counter {

    //-- Constructor del objeto
    //-- Inicialización de las propiedades
    constructor(display) {

        //-- Valor del contador
        this.valor = 0;

        //-- Almacenar su display
        this.display = display;
    }

    //-- Método inc para actualizar el contador
    //-- Y mostrarlo en el display
    inc(value) {
        this.valor += value;
        this.display.innerHTML = this.valor;
    }
}

Cronometrando con setInterval

En javascript podemos ejecutar acciones temporizadas por medio de la función setinterval, que llama a una función cada cierto tiempo.

El siguiente programa se puede probar directamente en la consola de javascript del navegador.

Primero definimos la función que queremos que se ejecute cada cierto tiempo.

La podemos llamar como queramos:

function timer() {
  console.log("Tic")
}

Simplemente haremos que salga un mensaje por la consola cada vez que se llama.

Ahora la activamos con setInterval para que se ejecute cada segundo (1000 milisegundos)

t1 = setInterval(timer, 1000)

Al hacerlo, veremos cómo la consola se actualiza cada segundo:

Para desactivarlo hay que llamar a la función clearInterval y pasarle como argumento el temporizador obtenido en su creación:

clearInterval(t1)

Ejemplo 7: Cronómetro

Vamos a implementar un cronómetro que me muestre en la pantalla el número centésimas, segundos y minutos transcurridos.

En este cronómetro definimos tres acciones: Comenzar (start), parar (stop) y poner a cero (reset).

Lo modelamos como una clase que tiene con estos tres métodos definidos.

Esta clase la metemos en el fichero crono.js:

//-- Clase cronómetro
class Crono {

    //-- Constructor. Hay que indicar el 
    //-- display donde mostrar el cronómetro
    constructor(display) {
        this.display = display;

        //-- Tiempo
        this.cent = 0, //-- Centésimas
        this.seg = 0,  //-- Segundos
        this.min = 0,  //-- Minutos
        this.timer = 0;  //-- Temporizador asociado
    }

    //-- Método que se ejecuta cada centésima
    tic() {
        //-- Incrementar en una centesima
        this.cent += 1;

        //-- 100 centésimas hacen 1 segundo
        if (this.cent == 100) {
        this.seg += 1;
        this.cent = 0;
        }

        //-- 60 segundos hacen un minuto
        if (this.seg == 60) {
        this.min = 1;
        this.seg = 0;
        }

        //-- Mostrar el valor actual
        this.display.innerHTML = this.min + ":" + this.seg + ":" + this.cent
    }

    //-- Arrancar el cronómetro
    start() {
       if (!this.timer) {
          //-- Lanzar el temporizador para que llame 
          //-- al método tic cada 10ms (una centésima)
          this.timer = setInterval( () => {
              this.tic();
          }, 10);
        }
    }

    //-- Parar el cronómetro
    stop() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
    }

    //-- Reset del cronómetro
    reset() {
        this.cent = 0;
        this.seg = 0;
        this.min = 0;

        this.display.innerHTML = "0:0:0";
    }
}

El objeto crono tiene cuatro métodos:

  • tic(), que es el que se ejecuta cada centésima (10 milisegundos), y es el que recalcula el tiempo, además de actualizar el display con el nuevo valor.
  • start(): Arrancar el cronómetro. Se encarga de llamar a setInterval, pero sólo una vez. e usa el atributo timer para saber si ya estaba en marcha o no.
  • stop(): Parar el cronómetro. Se elimina el temporizador, si es que estaba lanzado.
  • reset(): Poner a cero el cronómetro.

La aplicación del cronómetro la definimos en el fichero Ej-07.js.

//-- Elementos de la gui
const gui = {
    display : document.getElementById("display"),
    start : document.getElementById("start"),
    stop : document.getElementById("stop"),
    reset : document.getElementById("reset")
}

console.log("Ejecutando JS...");

//-- Definir un objeto cronómetro
const crono = new Crono(gui.display);

//---- Configurar las funciones de retrollamada

//-- Arranque del cronometro
gui.start.onclick = () => {
    console.log("Start!!");
    crono.start();
}
  
//-- Detener el cronómetro
gui.stop.onclick = () => {
    console.log("Stop!");
    crono.stop();
}

//-- Reset del cronómetro
gui.reset.onclick = () => {
    console.log("Reset!");
    crono.reset();
}

Creamos el objeto gui que contiene a todos los elementos de la interfaz html: el display y los tres botones.

Luego creamos el objeto crono y configuramos las funciones de retrollamada de los botones de la interfaz.

Por último, en el fichero HTML tenemos que indicarle que cargue los dos ficheros javascript.

Primero el de la clase y luego el principal:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="crono.js"></script>
    <script src="Ej-07.js" defer></script>
    <title>Ejemplo 7</title>
</head>
<body>
    <p id="display">0:0:0</p>
    <button type="button" id="start">Start</button>
    <button type="button" id="stop">Stop</button>
    <button type="button" id="reset">Reset</button>
</body>
</html>

Este es un ejemplo de funcionamiento:

Arrays

Los arrays nos permiten agrupar elementos heterogéneos, y enumerarlos.

A diferencia de otros lenguajes, los elementos almacenados en un array pueden ser cualquier objeto javascript: una cadena, una función, un objeto...

Este es un ejemplo de array de cadenas:

const identificador = ["display", "boton1", "boton2"];

Accedemos a los elementos usando su índice.

El primer elemento es el 0.

Así, el elemento identificador[0] representa a la cadena "display".

El atributo length nos da su tamaño.

Los arrays los podemos recorrer de diversas formas.

Este ejemplo recorre los tres identificadores anteriores e imprime sus cadenas.

for (let i=0; i < identificador.length; i++) {
  console.log(identificador[i])
}

Utilizando el método forEach(), que ejecuta la función de callback para cada elemento del array.

identificador.forEach((element, index)=>{
  console.log(element);
});

O con esta construcción, que recorre todo los objetos del array:

for (let element of identificador) {
  console.log(element);
}

WooClap Time!

Teclado numérico

Vamos a ver algunas cosas útiles para hacer la práctica 2.

Ejemplo 8: Agrupando los botones por clase

Las funciones de retrollamada de los botones del teclado numérico se pueden establecer como ya sabemos: asignamos un identificador único (id) a cada botón y utilizamos el método document.getElementByID() para acceder al componente.

Sin embargo, en aplicaciones en las que hay muchos botones, este método es poco recomendable.

Es mejor automatizarlo, haciendo un bucle que lea todos los botones y que establezca sus funciones de retrollamada.

Así, todos los botones que representan números en nuestro teclado, lo podemos agrupar en la clase dígito.

A través del método document.getElementsByClassName() obtenemos una colección con todos los elementos que pertenecen a esa clase.

Es decir, todos los botones que representan un dígito.

Además, la etiqueta button permite usar el atributo value para asignar un valor a cada botón.

En este fichero HTML tenemos definidos dos botones para los dígitos 1 y 2 respectivamente.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="Ej-08.js" defer></script>
    <title>Ejemplo 8</title>
</head>
<body>
    <p id="display">0</p>
    <button type="button" value="1" class="digito">1</button>
    <button type="button" value="2" class="digito">2</button>
</body>
</html>

Este programa javascript recorre los dos botones y muestra sus valores:

console.log("Ejecutando JS...");

const botones = document.getElementsByClassName("digito");

for (let boton of botones) {
    console.log("Boton: " +  boton.value)
}

Al ejecutarlo, vemos en la consola sus valores:

Ejemplo 9: Función de retrollamada para todos los miembros de una clase.

Ahora que sabemos recorrer todos los elementos de la misma clase, basta con establecer la función de retrollamada dentro del bucle.

Así conseguimos que se llame a esa función cuando se pulse cualquier de los botones de la misma clase.

La función de retrollamada del evento clic recibe el argumento ev, que tiene una propiedad llamada target que es el botón que ha sido pulsado.

Invocamos a la función digito() pasándole el valor del botón, obtenido de ev.target.value

console.log("Ejecutando JS...");

const botones = document.getElementsByClassName("digito")

//-- Función de retrollamada de los botones
//-- botones de la clase dígito
function digito(value)
{
  console.log("Valor: " + value);
}

for (let boton of botones) {

  //-- Establecer la función de llamada del botón i
  //-- El parámetro ev.target contiene el boton
  //-- que ha recibido el clic
  boton.onclick = (ev) => {
    digito(ev.target.value)
  }
}

Al ejecutarlo se muestra en la consola el valor del botón apretado.

Lo interesante de este método, es que ahora podemos añadir otro botón de la misma clase dígito en el HTML y se mostrará también su valor en la consola sin modificar el programa javascript.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="Ej-09.js" defer></script>
    <title>Ejemplo 9</title>
</head>
<body>
    <p id="display">0</p>
    <button type="button" value="1" class="digito">1</button>
    <button type="button" value="2" class="digito">2</button>
    <button type="button" value="3" class="digito">3</button>
</body>
</html>

Simplemente hemos añadido un nuevo botón en el HTML.

Ahora recargamos la página y probamos:

Autor

Jesús Parrado Alameda (jesusgpa)

Creditos

Licencia

Enlaces

⚠️ **GitHub.com Fallback** ⚠️