PageSourceSearch

https://m0ve.app/assets/controllers/live_timer_controller-725f74f7…2ba448593ebe0c0e896e15cb61a041819adff0c1.js

js m0ve.app collected 2026-10-07 11:38:53 UTC 2,338 bytes, 64 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// A running time-tracker total that actually moves.
4//
5// The server renders the completed total and, when a timer is running, the
6// instant it started. This adds the two together once a second, so "hours
7// logged" includes the timer you're currently running instead of jumping only
8// when you stop it.
9//
10// Values
11//   startAt      ISO8601 instant the running timer began. Absent → static.
12//   baseSeconds  Already-completed seconds this readout should include.
13//   rate         Hourly rate; when set, the amount target ticks alongside.
14//   showSeconds  HH:MM:SS (an entry) vs HH:MM (a rolled-up total).
15//
16// The interval is cleared on disconnect: Turbo restores cached pages, and a
17// timer still pointing at a detached element leaks on every visit.
18export default class extends Controller {
19  static targets = ["duration", "amount"]
20  static values = {
21    startAt: String,
22    baseSeconds: { type: Number, default: 0 },
23    rate: { type: Number, default: 0 },
24    showSeconds: { type: Boolean, default: true }
25  }
26
27  connect() {
28    this.start = this.startAtValue ? new Date(this.startAtValue) : null
29    if (this.start && isNaN(this.start.getTime())) this.start = null
30
31    this._tick()
32    // No running timer means nothing changes — render once and stop, rather
33    // than waking up every second to write the same string.
34    if (this.start) this.timer = setInterval(() => this._tick(), 1000)
35  }
36
37  disconnect() {
38    if (this.timer) clearInterval(this.timer)
39    this.timer = null
40  }
41
42  _tick() {
43    const running = this.start ? Math.max(0, Math.floor((Date.now() - this.start.getTime()) / 1000)) : 0
44    const total = this.baseSecondsValue + running
45
46    if (this.hasDurationTarget) {
47      this.durationTarget.textContent = this._format(total)
48    }
49
50    if (this.hasAmountTarget && this.rateValue > 0) {
51      const amount = (total / 3600) * this.rateValue
52      this.amountTarget.textContent = `$${amount.toFixed(2)}`
53    }
54  }
55
56  _format(totalSeconds) {
57    const hours = Math.floor(totalSeconds / 3600)
58    const minutes = Math.floor((totalSeconds % 3600) / 60)
59    const pad = n => String(n).padStart(2, "0")
60
61    if (!this.showSecondsValue) return `${pad(hours)}:${pad(minutes)}`
62    return `${pad(hours)}:${pad(minutes)}:${pad(totalSeconds % 60)}`
63  }
64};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.