import { Controller } from "@hotwired/stimulus" import { differenceInDays } from "helpers/date_helpers" const DEFAULT_LOCALE = "en-US" export default class extends Controller { static targets = [ "time", "date", "datetime", "shortdate", "ago", "indays", "daysago", "agoorweekday", "timeordate" ] static values = { refreshInterval: Number } static classes = [ "local-time-value"] #timer initialize() { this.timeFormatter = new Intl.DateTimeFormat(DEFAULT_LOCALE, { timeStyle: "short" }) this.dateFormatter = new Intl.DateTimeFormat(DEFAULT_LOCALE, { dateStyle: "long" }) this.shortdateFormatter = new Intl.DateTimeFormat(DEFAULT_LOCALE, { month: "short", day: "numeric" }) this.datetimeFormatter = new Intl.DateTimeFormat(DEFAULT_LOCALE, { timeStyle: "short", dateStyle: "short" }) this.agoFormatter = new AgoFormatter() this.daysagoFormatter = new DaysAgoFormatter() this.datewithweekdayFormatter = new Intl.DateTimeFormat(DEFAULT_LOCALE, { weekday: "long", month: "long", day: "numeric" }) this.datewithweekdayFormatter = new Intl.DateTimeFormat(DEFAULT_LOCALE, { weekday: "long", month: "long", day: "numeric" }) this.indaysFormatter = new InDaysFormatter() this.agoorweekdayFormatter = new DaysAgoOrWeekdayFormatter() this.timeordateFormatter = new TimeOrDateFormatter() } connect() { this.#timer = setInterval(() => this.#refreshRelativeTimes(), 30_000) this.refreshAll() } disconnect() { clearInterval(this.#timer) } refreshAll() { this.constructor.targets.forEach(targetName => { this.targets.findAll(targetName).forEach(target => { this.#formatTime(this[`${targetName}Formatter`], target) }) }) } refreshTarget(event) { const target = event.target; const targetName = target.dataset.localTimeTarget this.#formatTime(this[`${targetName}Formatter`], target) } #refreshRelativeTimes() { this.agoTargets.forEach(target => { this.#formatTime(this.agoFormatter, target) }) } #formatTime(formatter, target) { const dt = new Date(target.getAttribute("datetime")) target.innerHTML = formatter.format(dt) target.title = this.datetimeFormatter.format(dt) } } class AgoFormatter { format(dt) { const now = new Date() const seconds = (now - dt) / 1000 const minutes = seconds / 60 const hours = minutes / 60 const days = hours / 24 const weeks = days / 7 const months = days / (365 / 12) const years = days / 365 if (years >= 1) return this.#pluralize("year", years) if (months >= 1) return this.#pluralize("month", months) if (weeks >= 1) return this.#pluralize("week", weeks) if (days >= 1) return this.#pluralize("day", days) if (hours >= 1) return this.#pluralize("hour", hours) if (minutes >= 1) return this.#pluralize("minute", minutes) return "Less than a minute ago" } #pluralize(word, quantity) { quantity = Math.floor(quantity) const suffix = (quantity === 1) ? "" : "s" return `${quantity} ${word}${suffix} ago` } } class DaysAgoFormatter { format(date) { const days = differenceInDays(date, new Date()) if (days <= 0) return styleableValue("today") if (days === 1) return styleableValue("yesterday") return `${styleableValue(days)} days ago` } } class DaysAgoOrWeekdayFormatter { format(date) { const days = differenceInDays(date, new Date()) if (days <= 1) { return new DaysAgoFormatter().format(date) } else { return new Intl.DateTimeFormat(DEFAULT_LOCALE, { weekday: "long", month: "long", day: "numeric" }).format(date) } } } class InDaysFormatter { format(date) { const days = differenceInDays(new Date(), date) if (days <= 0) return styleableValue("today") if (days === 1) return styleableValue("tomorrow") return `in ${styleableValue(days)} days` } } class TimeOrDateFormatter { format(date) { const days = differenceInDays(date, new Date()) if (days >= 1) { return new Intl.DateTimeFormat(DEFAULT_LOCALE, { month: "short", day: "numeric" }).format(date) } else { return new Intl.DateTimeFormat(DEFAULT_LOCALE, { timeStyle: "short" }).format(date) } } } function styleableValue(value) { return `${value}` }