-1

There is a word-translation card page which includes:

  1. A 3-second countdown that runs immediately after the page loads and when you move to a new word after press the 'Correct' button.
  2. Word and his translate which appears after 'Excellent' button pressed. This button stops and hide the counter.
  3. Button 'Show answer' which appears if 'Excellent' didn't pressed in 3 sec.
  4. Buttons 'Wrong' and 'Correct' which appears if 'Excellent' or 'Show answer' buttons are pressed.

The problem is how the countdown works (unstable). Sometimes it doesn't restart after clicking "Correct". I tried manage countdown in separate function, but this approach provides much more issues. So now there is a timer that is called globally and a timer that is called when you click on the "Correct" button.I think the problem is near timerId. I will be glad to any comments and ideas on the code.

<div class="deck-container">
    <div class="words-container">
        <h2 id="primary-word"></h2>
        <h2 id="secondary-word"></h2>
    </div>

    <div class="btn-container">
        <p id="timer-count">3</p>
        <button id="btn-excellent" onClick="excellent()">Excellent!</button>
        <button id="btn-show-answer" onClick="showAnswerF()">Show Answer</button>
        <button id="btn-wrong">Wrong</button>
        <button id="btn-correct" onClick="correctF()">Correct</button>
    </div>
</div>
let currentWord = 0
let timerCount = 3
let fetched_data = {}

async function getDeck () {
    let response = await fetch('/api/deck_words/2/', {
        method: 'get',
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
            'Content-Type': 'application/json'
            }
        }
    )
    let data = await response.json()
    let primaryElement = document.getElementById('primary-word')
    primaryElement.innerText = await data[currentWord]['primary_word']
    let secondaryElement = document.getElementById('secondary-word')
    secondaryElement.innerText = await data[currentWord]['secondary_word']
    return data
}

fetched_data = getDeck()

const getData = async () => {
    return await getDeck()
}

data = getData();

function countDecrease() {
    timerCount --
    if (timerCount > 0) {
        document.getElementById("timer-count").innerHTML = timerCount
    } else {
         hideExcellent()
    }
}

function hideExcellent() {
    showElement(true,'btn-excellent')
    showElement(true,'timer-count')
    showElement(false,'btn-show-answer')
}

let timerId = setInterval(() => countDecrease(), 1000)

setTimeout(() => {
    clearInterval(timerId)
}, 3000)

function showElement(showProperty, elementClass) {
    showProperty = !showProperty
    let element = document.getElementById(elementClass)
    element.style.display = (showProperty  === true) ? "inline-block" : "none";
}

function showAnswerF() {
    showElement(true,'btn-show-answer')
    showElement(false,'secondary-word')
    showElement(false,'btn-wrong')
    showElement(false,'btn-correct')
}

function excellent() {
    showElement(true,'timer-count')
    showElement(true,'btn-excellent')
    showElement(false,'btn-wrong')
    showElement(false,'btn-correct')
    showElement(false,'secondary-word')
    clearInterval(timerId)
    timerCount = 3
}

function correctF() {
    currentWord++
    const changeWords = () => {
        fetched_data.then((data) => {
        document.getElementById('primary-word').innerText = data[currentWord]['primary_word']
        document.getElementById('secondary-word').innerText = data[currentWord]['secondary_word']
        document.getElementById("timer-count").innerText = '3'

        timerCount = 3

        timerId = setInterval(() => countDecrease(), 1000)

        setTimeout(() => {
            clearInterval(timerId)
        }, 3000)
      })
    }
    changeWords()

    let countElement = document.getElementById('timer-count')
    countElement.style.display = "block"
    showElement(false,'btn-excellent')
    showElement(true,'btn-wrong')
    showElement(true,'btn-correct')
    showElement(true,'secondary-word')
}
CarloDiPalma
  • 63
  • 2
  • 10

2 Answers2

1

I think this would be better handled with an async function, maybe like so.

const timerCount = document.querySelector("#timer-count")
const reset = document.querySelector("button")

function delay(ms) {
    return new Promise(res => setTimeout(res, ms))
}

async function countDown(signal) {
    const aborted = new Promise(resolve => signal.addEventListener("abort", resolve))
    for (let i = 10; i >= 0 && signal?.aborted != true; --i) {
        timerCount.textContent = i
        await Promise.race([delay(1000), aborted])
    }
    timerCount.textContent = ""
}

async function startCountdown() {
    const ac = new AbortController()
    const abort = () => ac.abort()
    reset.addEventListener("click", abort, { once: true })
    reset.textContent = "Cancel"
    await countDown(ac.signal)
    reset.removeEventListener("click", abort)
    reset.addEventListener("click", startCountdown, { once: true })
    reset.textContent = "Start"
}

startCountdown()
<p id="timer-count"></p>
<button>Start</button>

Alternatively, you might want to model the countdown as an object that implements EventTarget.

const timerCount = document.querySelector("#timer-count")
const btn = document.querySelector("button")

class Timer extends EventTarget {
    #value; #tick_rate; #enabled; #interval_handle
    constructor(tick_rate = 1000) {
        super()
        this.#value = 0
        this.#tick_rate = tick_rate
        this.#enabled = false
        this.#interval_handle = null
    }
    get value() {
        return this.#value
    }
    set value(value) {
        this.#value = value
        this.dispatchEvent(new Event("update"))
    }
    get tick_rate() {
        return this.#tick_rate
    }
    get enabled() {
        return this.#enabled
    }
    start() {
        if (this.#enabled) return
        this.#enabled = true
        this.#interval_handle = setInterval(Timer.#tick, this.#tick_rate, this)
        this.dispatchEvent(new Event("start"))
    }
    stop() {
        if (!this.#enabled) return
        this.#enabled = false
        clearInterval(this.#interval_handle)
        this.dispatchEvent(new Event("stop"))
    }
    static #tick(timer) {
        timer.value = Math.max(0, timer.value - 1)
        if (timer.value == 0) timer.stop()
    }
}

const timer = new Timer()

timer.addEventListener("start", function() {
    btn.textContent = "Stop"
})

timer.addEventListener("stop", function() {
    timerCount.textContent = ""
    btn.textContent = "Start"
})

timer.addEventListener("update", function() {
    timerCount.textContent = timer.value
})

btn.addEventListener("click", function() {
    if (timer.enabled == false) {
        timer.value = 5
        timer.start()
    } else {
        timer.stop()
    }
})
<p id="timer-count"></p>
<button>Start</button>
Chris_F
  • 4,991
  • 5
  • 33
  • 63
  • But in addition to the reset button, I need a button that stops and hides the countdown, or resets countdown before his end. – CarloDiPalma Jan 27 '23 at 03:27
  • 1
    @pachvo OK, so add an `AbortController` since its purpose is to cancel async tasks. Example updated. – Chris_F Jan 27 '23 at 04:08
0

Here's a countdown. The first count is after 1 second.

let count = 3;
let timer = [];

const start = () => {
  new Array(count).fill(true).forEach((_,i) => { 
    timer.push(setTimeout(() => console.log('count',count - i),(i+1) * 1000))
  })
}

const stop = () => timer.forEach(clearTimeout);
<button onclick="stop()">STOP</button>
<button onclick="start()">START</button>
user3094755
  • 1,561
  • 16
  • 20