DEV Community

LogUI 🌈
LogUI 🌈

Posted on

Pomodoro con React Hooks

¡Acompáñame a construir un Pomodoro con React Hooks!

  1. ¿Qué es Pomodoro?
  2. Preparando el terreno
  3. Componentes
  4. Componentes: Break y Session
  5. Componentes: Timer
  6. Componentes: App

Nota: Este tutorial no va a presuponer que tengas ningún conocimiento, así que si ya sabes React es posible que resulte aburrido y quieras ir directamente al código del proyecto terminado.

¿Qué es Pomodoro?

Se trata de una metodología que consiste en intercalar tiempos más o menos prolongados de concentración, con unos minutos de descanso.

Se utiliza mucho para estudiar, pero muchas otras personas lo usan también para programar, ya que ayuda a no olvidarnos de beber, comer, descansar la vista o simplemente estirar las piernas.

Hoy en día disponemos de muchas aplicaciones que nos permiten seguir este método de una forma muy cómoda. Este es el funcionamiento que tienen casi todas en común:

  1. Marcas el tiempo que quieres que dure la sesión y el de los descansos.
  2. Pulsas "Start" y empieza la cuenta atrás.
  3. Cuando el tiempo se agota, recibes una notificación que por lo general suele ser auditiva.
  4. Aquí hay diferentes opciones. Algunas aplicaciones simplemente cambian el modo (de sesión a descanso y viceversa) y empiezan la cuenta atrás del modo correspondiente. Otras detienen el temporizador pero no siguen corriendo hasta que no es activado manualmente. En este tutorial vamos a hacerlo de la primera manera.
  5. ¡Y ya está! Puedes añadir más o menos características, pero solo con eso ya tienes una aplicación funcional.

Preparando el terreno

[ Si no puedes o quieres realizar el proyecto en el dispositivo en el que te encuentras, también puedes hacer fork a este proyecto en Codepen. El botón de fork lo encuentras en la parte de abajo de la página, por la zona de la derecha.

Puedes, entonces, pasar a la sección de Componentes. Aquí no necesitas importar nada, y ya he declarado los componentes que necesitas de React Bootstrap, así que solo tienes que usarlos.]

Así que vamos a empezar. Lo primero que necesitamos es crear un proyecto con React en local:

npx create-react-app pomodoro-react
cd pomodoro-react
npm start
Enter fullscreen mode Exit fullscreen mode

! Ten en cuenta que necesitas tener Node instalado para que funcione.

Esto hará que se abra un servidor en http://localhost:3000/ donde podrás ver que React ya está funcionando. Para terminar de comprobar que todo va correctamente, vamos a ir a la carpeta src y abrir App.js con nuestro IDE o editor de texto favorito y a cambiar el código que tenemos por este:

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <p>Holi, soy React</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Al guardar el archivo, el servidor debería actualizarse y mostrarnos solamente "Holi, soy React". Si es así, podemos pasar al siguiente paso: instalar React Bootstrap.

Para ello vamos a usar el siguiente comando:

npm install react-bootstrap bootstrap

Comprobemos de nuevo de una manera sencilla que Bootstrap funciona cambiando algunas cosas en App.js:

import React from 'react';
import logo from './logo.svg';
import './App.css';
import {Button} from 'react-bootstrap';

function App() {
  return (
    <div className="App">
      <Button>Holi, soy React</Button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Para que funcione, necesitas importar los estilos de Bootstrap en el archivo index.js (también en la carpeta dist) incluyendo la siguiente línea justo antes del index.css:

import 'bootstrap/dist/css/bootstrap.css';
Enter fullscreen mode Exit fullscreen mode

Ahora nuestro texto simplón debería haberse convertido en un botón azul. Si es así, ¡enhorabuena! Ya podemos empezar a programar.

Componentes

Vamos a utilizar cuatro componentes: App, Break, Session y Timer. Dado que salvo App van a ser muy pocas líneas de código, vamos a escribir todo en el propio archivo App.js, pero si crees que para ti es más cómodo o vas a añadir más funcionalidades es posible que quieras tener cada componente en un archivo separado. En este caso lo único que tienes que recordar es importarlos a App.js.

# Break y Session

Aunque son dos componentes distintos, la lógica de ambos es la misma y con que construyamos uno con solo cambiar un par de nombres tendremos el otro.

Vamos a utilizar Break de ejemplo. Este componente solo va a encargarse de recibir y mostrar la cantidad de minutos que queremos para los descansos.

Para ello, usamos nuestro primer Hook: useState. Para nuestra comodidad, vamos a importarlo directamente junto a React. Entre unas cosas y otras, App.js se quedaría en algo parecido a esto a estas alturas:

import React, {useState} from 'react';
import logo from './logo.svg';
import './App.css';
import {Button, Col, Container, Row} from 'react-bootstrap';

const Break = () => {
    const [breakLength, setBreakLength] = useState(5 * 60);
    return (
        <Col>
            <p>Break</p>
            <Button variant="info">-</Button>
            <span>{breakLength / 60}</span>
            <Button variant="info">+</Button>
        </Col>
    )
}

const App = () => {
  return (
    <Container className="App">
      <Row>
        <Break/>
      </Row>
    </Container>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Fíjate que hemos importado Col, Container y Row de React Bootstrap también para que todo gire correctamente.

Tenemos dos botones para sumar y restar, pero aún no hacen nada. ¡solucionemos eso creando dos funciones!

const Break = () => {
    const [breakLength, setBreakLength] = useState(5 * 60);

    function decrementBreakLength() {
        const decreasedBreakLength = breakLength - 60 > 60 ? breakLength - 60 : 60;
        setBreakLength(decreasedBreakLength);
    }

    function incrementBreakLength() {
        const incrementedBreakLength =
        breakLength + 60 <= 60 * 60 ? breakLength + 60 : 60 * 60;
        setBreakLength(incrementedBreakLength);
    }
    return (
        <Col>
            <p>Break</p>
            <Button variant="info" onClick={decrementBreakLength}>-</Button>
            <span>{breakLength / 60}</span>
            <Button variant="info" onClick={incrementBreakLength}>+</Button>
        </Col>
    )
}
Enter fullscreen mode Exit fullscreen mode

Cuando los botones de restar o sumar son pulsados, llamamos a decrementBreakLength o incrementBreakLength respectivamente. Ambas funciones hacen el cálculo correspondiente (restando o sumando 1 minuto) y actualizan el valor de breakLength usando setBreakLength. ¡ya tenemos un valor que cambia dinámicamente! Hemos puesto un límite para que no pueda ser menor de un minuto ni mayor a una hora.

Como dije antes, para tener nuestro componente Session solo tenemos que copiar/pegar Break y cambiar los términos correspondientes para que todo coincida:

const Session = () => {
    const [sessionLength, setSessionLength] = useState(25 * 60);

    function decrementSessionLength() {
        const decreasedSessionLength = sessionLength - 60 > 60 ? sessionLength - 60 : 60;
        setSessionLength(decreasedSessionLength);
    }

    function incrementSessionLength() {
        const incrementedSessionLength =
        sessionLength + 60 <= 60 * 60 ? sessionLength + 60 : 60 * 60;
        setSessionLength(incrementedSessionLength);
    }
    return (
        <Col>
            <p>Session</p>
            <Button variant="info" onClick={decrementSessionLength}>-</Button>
            <span>{sessionLength / 60}</span>
            <Button variant="info" onClick={incrementSessionLength}>+</Button>
        </Col>
    )
}
Enter fullscreen mode Exit fullscreen mode

! recuerda añadir Session a tu componente App, justo debajo de Break.

# Timer

Usaremos este componente para mostrar los minutos y segundos que quedan para que termine la sesión o el descanso activados.

Aquí vamos a empezar a ver la necesidad de llevar las funcionalidades de Break y Session al componente padre (App), ya que necesitaremos acceder al valor de breakLength y sessionLength para que desde Timer sepamos cuál es el tiempo que tenemos que mostrar.

Así que lo primero que vamos a hacer va a ser llevarnos los hooks y las funciones y a pasar lo que sea necesario como propiedades. El código completo sería el siguiente:

const Break = (props) => {
    const { increment, decrement, length } = props;
    return (
        <Col>
            <p>Break</p>
            <Button variant="info" onClick={decrement}>-</Button>
            <span>{length / 60}</span>
            <Button variant="info" onClick={increment}>+</Button>
        </Col>
    )
}

const Session = (props) => {
    const { increment, decrement, length } = props;
    return (
        <Col>
            <p>Session</p>
            <Button variant="info" onClick={decrement}>-</Button>
            <span>{length / 60}</span>
            <Button variant="info" onClick={increment}>+</Button>
        </Col>
    )
}

const App = () => {
    const [breakLength, setBreakLength] = useState(5 * 60);
    const [sessionLength, setSessionLength] = useState(25 * 60);

    function decrementBreakLength() {
        const decreasedBreakLength = breakLength - 60 > 60 ? breakLength - 60 : 60;
        setBreakLength(decreasedBreakLength);
    }

    function incrementBreakLength() {
        const incrementedBreakLength =
        breakLength + 60 <= 60 * 60 ? breakLength + 60 : 60 * 60;
        setBreakLength(incrementedBreakLength);
    }

    function decrementSessionLength() {
        const decreasedSessionLength = sessionLength - 60 > 60 ? sessionLength - 60 : 60;
        setSessionLength(decreasedSessionLength);
    }

    function incrementSessionLength() {
        const incrementedSessionLength =
        sessionLength + 60 <= 60 * 60 ? sessionLength + 60 : 60 * 60;
        setSessionLength(incrementedSessionLength);
    }
  return (
    <Container className="App">
      <Row>
        <Break
          length={breakLength}
          decrement={decrementBreakLength}
          increment={incrementBreakLength}
        />
        <Session
          length={sessionLength}
          decrement={decrementSessionLength}
          increment={incrementSessionLength}
        />
      </Row>
    </Container>
  );
}
Enter fullscreen mode Exit fullscreen mode

Todo debería seguir funcionando igual. No necesitaremos volver a preocuparnos más por los componentes Break y Session. Ahora podemos centrarnos en lo que Timer nos puede aportar.

De momento, vamos a pasarle sessionLength como un valor estático:

    <Container className="App">
      <Timer time={sessionLength}/>
      <Row>
Enter fullscreen mode Exit fullscreen mode

y a mostrarlo:

const Timer = (props) => {
    const {time} = props;

    return (
        <div>
          <p>Session</p>
          <p>
            {time / 60}:00
          </p>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Volveremos a él luego para actualizarlo, tras conseguir que App pueda proporcionarle datos dinámicos.

# App

Aquí va todo el meollo de nuestra aplicación. No te preocupes, no es difícil y yo te lo voy a explicar lo mejor que sé. Vamos a dividir mentalmente este Componente en tres partes: useState, useEffect y funciones.

Usando useState vamos a declarar lo siguiente, justo debajo de breakLength y sessionLength:

      const [mode, setMode] = useState("session");
      const [timeLeft, setTimeLeft] = useState();
      const [isActive, setIsActive] = useState(false);
      const [timeSpent, setTimeSpent] = useState(0);
      const [beep] = useState(
        new Audio("https://freesound.org/data/previews/523/523960_350703-lq.mp3")
      );
      const [beepPlaying, setBeepPlaying] = useState(false);
Enter fullscreen mode Exit fullscreen mode

· mode nos va a indicar en qué modo se encuentra ahora mismo la aplicación (¿modo 'session' o modo 'break'?
· timeLeft es un parámetro para saber cuánto tiempo queda
· isActive va a marcar si el temporizador está corriendo o no
· timeSpent puede ser menos intuitivo fuera de contexto, pero es la variable que nos va a ir guardando el tiempo que ha pasado desde que pulsamos "Start". Es lo que nos sirve para calcular cuánto nos falta.
· beep es el sonido que usaremos para avisar cuando el temporizador llega a 0
· beepPlaying nos indica si el sonido está sonando o no.

Ahora vamos a por los useEffect. Recuerda importarlo en la primera línea junto a React y useState de esta forma:

import React, {useState, useEffect} from 'react';
Enter fullscreen mode Exit fullscreen mode

timeLeft va a ser nuestra referencia para saber cuánto tiempo nos queda y será el parámetro que pasemos al componente Timer, pero necesitamos una forma de que al actualizar breakLength y sessionLength timeLeft cambie su valor acorde a una u otra variable dependiendo del modo en el que estemos:

  useEffect(() => {
    setTimeLeft(mode == "session" ? sessionLength * 1000 : breakLength * 1000);
  }, [sessionLength, breakLength]);
Enter fullscreen mode Exit fullscreen mode

El useEffect que viene a continuación es probablemente el bloque más largo que va a tener nuestra aplicación, así que te lo voy a enseñar e ir explicando por partes:

· Necesitamos "escuchar" los cambios en isActive y timeSpent para saber cuándo se activa el temporizador y cómo van pasando los segundos.

useEffect(() => { }, [isActive, timeSpent]);
Enter fullscreen mode Exit fullscreen mode

· Lo primero que vamos a hacer va a ser declarar una variable interval a null.

let interval = null;
Enter fullscreen mode Exit fullscreen mode

· Comprobamos que el temporizador está activo y que además timeLeft es mayor a 1. Es decir, que aún queda tiempo que descontar. Si ambas condiciones se cumplen calcularemos el tiempo que queda y le sumaremos un segundo a timeSpent. Lo haremos además asignando todo esto a un intervalo para que el cambio se produzca tras 1 segundo. Y como este efecto está escuchando a timeSpent, se seguirá ejecutando hasta que isActive cambie y pase a ser false. Cuando esto ocurra, limpiaremos el intervalo.

    if (isActive && timeLeft > 1) {
      setTimeLeft(
        mode == "session"
          ? sessionLength * 1000 - timeSpent
          : breakLength * 1000 - timeSpent
      );

      interval = setInterval(() => {
        setTimeSpent((timeSpent) => timeSpent + 1000);
      }, 1000);
    } else {
      clearInterval(interval);
    }
Enter fullscreen mode Exit fullscreen mode

Ahora solamente necesitamos saber cuándo timeLeft ha llegado a cero, que será el momento de hacer que nuestro beep se reproduzca. También cambiamos el modo y pasamos la longitud del que corresponda a timeLeft para que el temporizador siga corriendo:

    if (timeLeft === 0) {
      beep.play();
      setBeepPlaying(true);
      setTimeSpent(0);
      setMode((mode) => (mode == "session" ? "break" : "session"));
      setTimeLeft(
        mode == "session" ? sessionLength * 1000 : breakLength * 1000
      );
    }
Enter fullscreen mode Exit fullscreen mode

Aquí te dejo el código completo:

useEffect(() => {
    let interval = null;

    if (isActive && timeLeft > 1) {
      setTimeLeft(
        mode == "session"
          ? sessionLength * 1000 - timeSpent
          : breakLength * 1000 - timeSpent
      );

      interval = setInterval(() => {
        setTimeSpent((timeSpent) => timeSpent + 1000);
      }, 1000);
    } else {
      clearInterval(interval);
    }
    if (timeLeft === 0) {
      beep.play();
      setBeepPlaying(true);
      setTimeSpent(0);
      setMode((mode) => (mode == "session" ? "break" : "session"));
      setTimeLeft(
        mode == "session" ? sessionLength * 1000 : breakLength * 1000
      );
    }
    return () => clearInterval(interval);
  }, [isActive, timeSpent]);
Enter fullscreen mode Exit fullscreen mode

¡Uf! Coge aire y bebe un poco de agua. Ya queda menos.

Este useEffect solamente va a encargarse de poner beepPlaying a false cuando beep deje de sonar.

  useEffect(() => {
    beep.addEventListener("ended", () => setBeepPlaying(false));
    return () => {
      beep.addEventListener("ended", () => setBeepPlaying(false));
    };
  }, []);
Enter fullscreen mode Exit fullscreen mode

Solo falta añadir algunas funciones a nuestro componente para que funcione como necesitamos:

function reset() {
    setBreakLength(5 * 60);
    setSessionLength(25 * 60);
    setTimeLeft(mode == "session" ? sessionLength * 1000 : breakLength * 1000);

    if (isActive) {
      setIsActive(false);
      setTimeSpent(0);
    }

    if (beepPlaying) {
      beep.pause();
      beep.currentTime = 0;
      setBeepPlaying(false);
    }
  }

  function toggleIsActive() {
    setIsActive(!isActive);
  }
Enter fullscreen mode Exit fullscreen mode

reset es un poco larga pero lo único que hace es restaurar los valores de breakLength y sessionLength a 5 y 25 minutos respectivamente. Además detiene el temporizador poniendo isActive a false y lo reinicia poniendo en cero la variable timeSpent.

toggleIsActive simplemente cambia isActive al valor opuesto (de false a true y viceversa).

Y ya solo nos queda añadir un par de componentes Button para que todo esto pueda funcionar.

   <Container className="text-center">
      <h1>Pomodoro Clock</h1>

      <Timer time={timeLeft} mode={mode} />
      <div className="buttons">
        <Button onClick={toggleIsActive} id="start_stop">
          {isActive ? "Pause" : "Start"}
        </Button>
        <Button onClick={reset} id="reset" variant="danger">
          Reset
        </Button>
      </div>
      <Row className="options">
        <Break
          length={breakLength}
          decrement={decrementBreakLength}
          increment={incrementBreakLength}
        />
        <Session
          length={sessionLength}
          decrement={decrementSessionLength}
          increment={incrementSessionLength}
        />
      </Row>
    </Container>
Enter fullscreen mode Exit fullscreen mode

Puedes ver que hemos añadido dos botones: uno para poner el temporizador en marcha (o pausarlo) y otro para poder resetearlo.

Pero espera, porque aunque nuestro temporizador funciona aún no estamos mostrando nada en pantalla. Timer sigue con valores estáticos. Vamos a solucionarlo:

const Timer = (props) => {
  const { time, mode } = props;

  const min = Math.floor(time / 1000 / 60);
  const sec = Math.floor((time / 1000) % 60);
  return (
    <div id="timer">
      <p id="timer-label">{mode}</p>
      <p id="time-left">
        {min}:{sec.toString().length === 1 ? "0" + sec : sec}
      </p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Simplemente vamos a coger el modo en el que estamos y el tiempo que queda de las propiedades. Para mostrar el tiempo necesitamos calcular los minutos y segundos, y luego poder añadir un cero en caso de que la cifra de segundos solo tenga un dígito.

¡Y ya está! Ya tienes tu Pomodoro.

const Break = (props) => {
  const { increment, decrement, length } = props;
  return (
    <Col md={4}>
      <p id="break-label">Break</p>
      <Button onClick={decrement} id="break-decrement" variant="info">
        -
      </Button>
      <span id="break-length">{length / 60}</span>
      <Button variant="info" onClick={increment} id="break-increment">
        +
      </Button>
    </Col>
  );
};

const Session = (props) => {
  const { increment, decrement, length } = props;
  return (
    <Col md={{ span: 4, offset: 4 }}>
      <p id="session-label">Session</p>
      <Button onClick={decrement} id="session-decrement" variant="info">
        -
      </Button>
      <span id="session-length">{length / 60}</span>
      <Button onClick={increment} id="session-increment" variant="info">
        +
      </Button>
    </Col>
  );
};

const Timer = (props) => {
  const { time, mode } = props;

  const min = Math.floor(time / 1000 / 60);
  const sec = Math.floor((time / 1000) % 60);
  return (
    <div id="timer">
      <p id="timer-label">{mode}</p>
      <p id="time-left">
        {min}:{sec.toString().length === 1 ? "0" + sec : sec}
      </p>
    </div>
  );
};

const App = () => {
  const [breakLength, setBreakLength] = React.useState(5 * 60);
  const [sessionLength, setSessionLength] = React.useState(25 * 60);
  const [mode, setMode] = React.useState("session");
  const [timeLeft, setTimeLeft] = React.useState();
  const [isActive, setIsActive] = React.useState(false);
  const [timeSpent, setTimeSpent] = React.useState(0);
  const [beep] = React.useState(
    new Audio("https://freesound.org/data/previews/523/523960_350703-lq.mp3")
  );
  const [beepPlaying, setBeepPlaying] = React.useState(false);

  /* ########## USE EFFECT HOOKS ########## */
  React.useEffect(() => {
    setTimeLeft(mode == "session" ? sessionLength * 1000 : breakLength * 1000);
  }, [sessionLength, breakLength]);

  React.useEffect(() => {
    let interval = null;

    if (isActive && timeLeft > 1) {
      setTimeLeft(
        mode == "session"
          ? sessionLength * 1000 - timeSpent
          : breakLength * 1000 - timeSpent
      );

      interval = setInterval(() => {
        setTimeSpent((timeSpent) => timeSpent + 1000);
      }, 1000);
    } else {
      clearInterval(interval);
    }
    if (timeLeft === 0) {
      beep.play();
      setBeepPlaying(true);
      setTimeSpent(0);
      setMode((mode) => (mode == "session" ? "break" : "session"));
      setTimeLeft(
        mode == "session" ? sessionLength * 1000 : breakLength * 1000
      );
    }
    return () => clearInterval(interval);
  }, [isActive, timeSpent]);

  React.useEffect(() => {
    beep.addEventListener("ended", () => setBeepPlaying(false));
    return () => {
      beep.addEventListener("ended", () => setBeepPlaying(false));
    };
  }, []);

  /* ########## FUNCTIONS ########## */
  function decrementBreakLength() {
    const decreasedBreakLength = breakLength - 60 > 60 ? breakLength - 60 : 60;
    setBreakLength(decreasedBreakLength);
  }

  function incrementBreakLength() {
    const incrementedBreakLength =
      breakLength + 60 <= 60 * 60 ? breakLength + 60 : 60 * 60;
    setBreakLength(incrementedBreakLength);
  }

  function decrementSessionLength() {
    const decreasedSessionLength =
      sessionLength - 60 > 60 ? sessionLength - 60 : 60;

    setSessionLength(decreasedSessionLength);
  }

  function incrementSessionLength() {
    const incrementedSessionLength =
      sessionLength + 60 <= 60 * 60 ? sessionLength + 60 : 60;
    setSessionLength(incrementedSessionLength);
  }

  function reset() {
    setBreakLength(5 * 60);
    setSessionLength(25 * 60);
    setTimeLeft(mode == "session" ? sessionLength * 1000 : breakLength * 1000);

    if (isActive) {
      setIsActive(false);
      setTimeSpent(0);
    }

    if (beepPlaying) {
      beep.pause();
      beep.currentTime = 0;
      setBeepPlaying(false);
    }
  }

  function toggleIsActive() {
    setIsActive(!isActive);
  }

  return (
    <Container className="text-center">
      <h1>Pomodoro Clock</h1>

      <Timer time={timeLeft} mode={mode} />
      <div className="buttons">
        <Button onClick={toggleIsActive} id="start_stop">
          {isActive ? "Pause" : "Start"}
        </Button>
        <Button onClick={reset} id="reset" variant="danger">
          Reset
        </Button>
      </div>
      <Row className="options">
        <Break
          length={breakLength}
          decrement={decrementBreakLength}
          increment={incrementBreakLength}
        />
        <Session
          length={sessionLength}
          decrement={decrementSessionLength}
          increment={incrementSessionLength}
        />
      </Row>
    </Container>
  );
};
Enter fullscreen mode Exit fullscreen mode

Ahora puedes trastear con él, como por ejemplo:

  • Cambiar el sonido o dar a elegir entre varios diferentes
  • Añadir otra forma de notificar el cambio de modo, ya que tal y como está ahora no podría ser usado por personas sordas. A mí se me ocurre, por ejemplo, cambiar el de la página dinámicamente, pero no he visto que sea posible en Codepen.
  • Fusionar tu Pomodoro con un proyecto de To-do list, de manera que puedas planificar en qué vas a invertir las sesiones.

Te dejo aquí de nuevo mi proyecto terminado y espero que hayas disfrutado construyendo esta pequeña aplicación.

Top comments (4)

Collapse
 
riviergrullon profile image
Rivier Grullon

Pense que tenia el traductor de pagina activado otra vez jajajja, pero me parecio genial el blog y ya que recientemente comence a usar la tecnica de pomodoro para organizar mi tiempo :)

Collapse
 
piguicorn profile image
LogUI 🌈 • Edited

¡me alegro mucho! Vi que ya había varios tutoriales en inglés para hacer Pomodoros y pensé que así sería más útil.

Muchas gracias por tu comentario y espero que te acuerdes de pasar por aquí el link al tuyo si lo haces.

Yo también lo estoy usando últimamente. ¿qué tal? ¿Te funciona? Confieso que ahora con la situación actual me cuesta más concentrarme y he subido el nivel. Ahora lo que pongo es gente en Youtube que se pone Pomodoros. insertar gif de Inception aquí

Collapse
 
gelopfalcon profile image
Falcon

Te felicito Logan, sigamos compartiendo contenido en español. Aunque sepamos inglés, es bueno crear contenido en nuestra lengua natal.