THU.JUN.18
2026
23:35:29
← back to modules MODULE · 04 · FINISH THE SITE
0 / 10 chapters complete · 0%

Make It Tick — The Live Clock & Focus Timer

This is the dashboard's first JavaScript ever. By the end you'll understand the exact code that makes the topbar clock tick every second and turns the Work tab's focus timer from a painted "25:00" into a real countdown with start, pause, and reset.
A browser clock needs setInterval to update once a second. The focus timer is a tiny state machine: a selected length, seconds remaining, and a ticking interval. All of it lives in app.js, loaded with defer at the bottom of index.php, and every piece bails out gracefully if its HTML isn't on the page.

Why the site had no JavaScript (and why that's about to change, carefully)

The dashboard was built with a rule at the very top of index.php: "No JavaScript. Navigation = query string." That's not laziness — it's a real philosophy. Server-rendered pages are simpler to reason about, they work with JS disabled, they don't break when a script errors, and there's no client/server state to keep in sync. For 90% of this dashboard, that rule is great and we're keeping it.

But a clock that updates every second, and a countdown that ticks down, are the 10% that physically cannot be server-rendered. The server renders the page once and then it's frozen — PHP has no way to reach back into an already-sent page and change the time. Only code running in the browser can do that. So we add a thin JavaScript layer for exactly these cases and nothing more. The fancy name for "works without JS, gets better with it" is progressive enhancement, and it's the grown-up way to add interactivity without throwing away the sturdy server-rendered foundation.

🐍 Python brain: setInterval(fn, 1000) is roughly "call this function every 1000ms." The closest Python feeling is a loop with time.sleep(1), or threading.Timer — except JavaScript is single-threaded with an event loop, so setInterval schedules the call and lets the browser keep doing everything else in between. Nothing blocks.

The ticking clock

This is the gentlest possible intro to JS, so let's savor it. In index.php the clock is just a span the server fills in once: <span class="clock" id="clock">14:32:07</span>. The server-rendered time is the fallback — if JavaScript never runs, you still see a (frozen) correct-at-load-time clock. Then app.js brings it to life:

function initClock() {
    var el = document.getElementById('clock');
    if (!el) return;                       // clock not on this page? bail.
    function tick() { el.textContent = fmtClock(new Date()); }
    tick();                                // paint immediately, don't wait 1s
    setInterval(tick, 1000);               // then every second forever
}

Three things worth noticing. First, that if (!el) return; guard — the clock is on every page, but writing the guard anyway is a habit that makes every module safe to run everywhere. Second, we call tick() once immediately so the clock updates the instant the page loads, instead of sitting on the stale server value for a full second. Third, textContent (not innerHTML) — we're setting plain text, and textContent can't accidentally inject HTML, so it's both faster and safer.

The focus timer — a tiny state machine

Here's where it gets fun. A countdown timer is the classic first "real" piece of interactive code because it forces you to think in state: a few variables that together describe "what's happening right now," plus functions that move between those states. Our timer tracks four things:

  • minutes — the selected block length (5, 25, or 50)
  • remaining — seconds left on the current countdown
  • running — are we currently counting down?
  • ticker — the handle returned by setInterval, so we can clearInterval to pause

The whole thing is just a handful of small functions that read and write those four variables. start() kicks off (or pauses) the countdown. reset() puts remaining back to the full length. finish() fires when we hit zero. Here's the heart of it:

function start() {
    if (running) {              // already running -> this click means "pause"
        stop();
        setLabel('⏸ PAUSED · ' + fmtMMSS(remaining));
        return;
    }
    if (remaining <= 0) reset();
    running = true;
    ticker = setInterval(function () {
        remaining -= 1;
        if (remaining <= 0) { finish(); return; }
        render();               // repaint "MM:SS"
    }, 1000);
}

The "click START again to pause" trick is the bit people find slick. One button, two meanings, decided by the running flag. When you pause, we clearInterval(ticker) so the countdown literally stops scheduling itself; the remaining value just sits there frozen until you hit start again. That's the beauty of holding state in variables — pausing is "stop the interval and don't touch the number."

The mode buttons (5 / 25 / 50) each carry a data-min attribute in the HTML, so clicking one is just "read that number, make it the new minutes, reset." We'll talk about data-* attributes properly in the next chapter — they're how the server hands little bits of data to the JavaScript.

How it's loaded — and why nothing explodes on other tabs

At the very bottom of index.php, just before </body>, there's one new line: <script src="app.js" defer></script>. The defer attribute tells the browser "download this in the background and run it after the HTML is parsed" — which means the script never blocks the page from rendering, and by the time it runs, all the elements it needs already exist.

Here's the clever part that keeps everything tidy: app.js has three modules (clock, timer, player) and it tries to start all three on every page. But each one begins with a guard like if (!disp) return;. On the Work tab there's no audio player, so the player module instantly bails. On the Music tab there's no timer display, so the timer module bails. Same script everywhere, but each piece only actually runs where its HTML exists. No errors, no "cannot read property of null," no per-page script juggling.

Let's confirm it's alive:

  1. Load the dashboard. Watch the topbar clock — the seconds should be counting up. If it's frozen, your app.js isn't loading (check the browser console, F12).
  2. Click the WORK tab. Hit the 50 min button — the display should jump to 50:00.
  3. Hit START. It counts down, and the button now says PAUSE. Hit it again — frozen. Again — resumes.
  4. Hit RESET — back to the full length.
Clock ticking, timer counting, START toggling to PAUSE, RESET working. If all four happen, your first JavaScript layer is live and progressive enhancement is officially a tool in your belt. Under the hood: a once-a-second interval driving the clock, and a four-variable state machine driving the timer. If the clock ticks but the timer doesn't, you're probably missing the id="timerDisp" hook in the markup.

▣ Mini Project: Wire the Timer Yourself

If the timer's already working, this is your chance to actually understand it by rebuilding it from the hooks up — the difference between "it works" and "I made it work." We're going to verify (or recreate) the two halves: the HTML hooks in index.php, and the JS in app.js.

  1. In index.php, find the Work tab's .timer-block. Make sure the display has id="timerDisp", the mode label has id="timerMode", each .tmode span has a data-min ("5"/"25"/"50") plus role="button" tabindex="0", and the START/RESET spans have id="timerStart" / id="timerReset".
  2. In app.js, confirm initTimer() grabs those elements, bails if timerDisp is missing, and wires click handlers onto the mode buttons + start + reset.
  3. Trace one full lifecycle in your head: page load → reset() shows 25:00 → click 50 → minutes = 50, reset → click START → interval ticks remaining down → hits 0 → finish() adds the done class and stops.
  4. Break it on purpose to learn: comment out the tick() call before setInterval in initClock. Reload. The clock now waits a full second before its first update. Put it back. (Seeing the bug teaches the fix.)

Stretch goals:

  • Play a sound when the timer finishes: new Audio('assets/audio/ding.mp3').play() inside finish().
  • Flash the page title: set document.title = '✓ done!' on finish so it grabs attention on a background tab.
  • Count completed sessions in a variable and show "×N today" — then note how it resets on reload. That itch ("why won't it remember?") is exactly what the database chapter scratches.

What you flexed: your first DOM manipulation (getElementById, textContent), setInterval/clearInterval, thinking in state instead of steps, a one-button-two-meanings toggle, and the feature-guard pattern that lets one script safely serve every page. Progressive enhancement: the page still works with JS off, it just gets nicer with it on.