Add relative-date-time component

This commit is contained in:
Daniel J. Summers 2024-12-11 20:22:10 -05:00
parent 52ec3f819c
commit 516a903565
2 changed files with 118 additions and 9 deletions

View File

@ -36,7 +36,9 @@ class Component
<section id=journalItems class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3" hx-target=this <section id=journalItems class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3" hx-target=this
hx-swap=outerHTML aria-label="Prayer Requests"><?php hx-swap=outerHTML aria-label="Prayer Requests"><?php
$spacer = '<span>&nbsp;</span>'; $spacer = '<span>&nbsp;</span>';
foreach ($reqs->items as /** @var Request $req */ $req) { ?> foreach ($reqs->items as /** @var Request $req */ $req) {
$lastPrayed = $req->lastPrayed;
$lastActivity = $lastPrayed->getOrDefault($req->history[0]->asOf); ?>
<div class=col> <div class=col>
<div class="card h-100"> <div class="card h-100">
<div class="card-header p-0 d-flex" role=toolbar> <div class="card-header p-0 d-flex" role=toolbar>
@ -58,10 +60,8 @@ class Component
<p class=request-text><?=htmlentities($req->currentText);?> <p class=request-text><?=htmlentities($req->currentText);?>
</div> </div>
<div class="card-footer text-end text-muted px-1 py-0"> <div class="card-footer text-end text-muted px-1 py-0">
<em><?php <em>last <?=$lastPrayed->map(fn() => 'prayed')->getOrDefault('activity')?>
$lastPrayed = $req->lastPrayed; <?=self::relativeDate($lastPrayed->getOrDefault($req->history[0]->asOf))?>
echo 'last ' . $lastPrayed->map(fn() => 'prayed')->getOrDefault('activity') . ' '
. self::relativeDate($lastPrayed->getOrDefault($req->history[0]->asOf)); ?>
</em> </em>
</div> </div>
</div> </div>
@ -115,9 +115,9 @@ class Component
public static function relativeDate(string $date): string public static function relativeDate(string $date): string
{ {
$parsed = new DateTimeImmutable($date); $parsed = new DateTimeImmutable($date);
$inZone = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone'])); $iso = $parsed->format('c');
return sprintf('<span title="%s">%s</span>', date_format($inZone, 'l, F j, Y \a\t g:ia T'), $title = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone']))->format('l, F j, Y \a\t g:ia T');
RelativeDate::between('now', $parsed)); return "<relative-date-time title=\"$title\" interval=5000>$iso</relative-date-time>";
} }
/** /**

View File

@ -1,7 +1,7 @@
"use strict" "use strict"
/** myPrayerJournal script */ /** myPrayerJournal script */
this.mpj = { window.mpj = {
/** /**
* Show a message via toast * Show a message via toast
* @param {string} message The message to show * @param {string} message The message to show
@ -102,3 +102,112 @@ htmx.on("htmx:configRequest", function (evt) {
}) })
mpj.deriveTimeZone() mpj.deriveTimeZone()
class RelativeDateTime extends HTMLElement {
static observedAttributes = ["interval"]
static #LessThanXMinutes = Symbol()
static #XMinutes = Symbol()
static #AboutXHours = Symbol()
static #XDays = Symbol()
static #AboutXMonths = Symbol()
static #XMonths = Symbol()
static #AboutXYears = Symbol()
static #OverXYears = Symbol()
static #AlmostXYears = Symbol()
static #messages = new Map([
[RelativeDateTime.#LessThanXMinutes, ['less than a minute', 'less than %d minutes']],
[RelativeDateTime.#XMinutes, ['a minute', '%d minutes']],
[RelativeDateTime.#AboutXHours, ['about an hour', 'about %d hours']],
[RelativeDateTime.#XDays, ['a day', '%d days']],
[RelativeDateTime.#AboutXMonths, ['about a month', 'about %d months']],
[RelativeDateTime.#XMonths, ['a month', '%d months']],
[RelativeDateTime.#AboutXYears, ['about a year', 'about %d years']],
[RelativeDateTime.#OverXYears, ['over a year', 'over %d years']],
[RelativeDateTime.#AlmostXYears, ['almost a year', 'almost %d years']],
])
static #aDay = 1440.0
static #almost2Days = 2520.0
static #aMonth = 43200.0
static #twoMonths = 86400.0
/** @type Date The date/time for this relative date */
#jsDate
/** @type ?number The interval timer for this element */
#timeOut = null
constructor() {
super();
}
#update() {
const now = new Date()
const minutes = Math.abs((this.#jsDate.getTime() - now.getTime()) / 60 / 1000);
const months = Math.round(minutes / RelativeDateTime.#aMonth);
const years = Math.floor(months / 12);
/** @type symbol */
let typ
/** @type number */
let nbr
if (minutes < 1.0) {
typ = RelativeDateTime.#LessThanXMinutes
nbr = 1
} else if (minutes < 45.0) {
typ = RelativeDateTime.#XMinutes
nbr = Math.round(minutes)
} else if (minutes < 90.0) {
typ = RelativeDateTime.#AboutXHours
nbr = 1
} else if (minutes < RelativeDateTime.#aDay) {
typ = RelativeDateTime.#AboutXHours
nbr = Math.round(minutes / 60)
} else if (minutes < RelativeDateTime.#almost2Days) {
typ = RelativeDateTime.#XDays
nbr = 1
} else if (minutes < RelativeDateTime.#aMonth) {
typ = RelativeDateTime.#XDays
nbr = Math.round(minutes / RelativeDateTime.#aDay)
} else if (minutes < RelativeDateTime.#twoMonths) {
typ = RelativeDateTime.#AboutXMonths
nbr = Math.round(minutes / RelativeDateTime.#aMonth)
} else if (months < 12) {
typ = RelativeDateTime.#XMonths
nbr = Math.round(minutes / RelativeDateTime.#aMonth)
} else if (months % 12 < 3) {
typ = RelativeDateTime.#AboutXYears
nbr = years
} else if (months % 12 < 9) {
typ = RelativeDateTime.#OverXYears
nbr = years
} else {
typ = RelativeDateTime.#AlmostXYears
nbr = years + 1
}
const tmpl = RelativeDateTime.#messages.get(typ)
const message = nbr === 1 ? tmpl[0] : tmpl[1].replace("%d", nbr.toString())
this.innerText = this.#jsDate < now ? `${message} ago` : `in ${message}`
}
connectedCallback() {
this.#jsDate = new Date(this.innerText)
this.#update()
}
disconnectedCallback() {
if (this.#timeOut) clearInterval(this.#timeOut)
}
attributeChangedCallback(name, oldValue, newValue) {
if (this.#timeOut) clearInterval(this.#timeOut)
if (this.#jsDate) this.#update()
this.#timeOut = setInterval(() => this.#update, parseInt(newValue))
}
}
customElements.define("relative-date-time", RelativeDateTime)