Refine relative date/time; add docs to files
This commit is contained in:
parent
516a903565
commit
75680fae00
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\UI;
|
||||
|
||||
@ -117,7 +123,7 @@ class Component
|
||||
$parsed = new DateTimeImmutable($date);
|
||||
$iso = $parsed->format('c');
|
||||
$title = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone']))->format('l, F j, Y \a\t g:ia T');
|
||||
return "<relative-date-time title=\"$title\" interval=5000>$iso</relative-date-time>";
|
||||
return "<relative-date-time title=\"$title\" interval=10000>$iso</relative-date-time>";
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\UI;
|
||||
|
||||
@ -56,7 +62,7 @@ class Layout
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
crossorigin=anonymous>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel=stylesheet>
|
||||
<link href=/style/style.css rel=stylesheet>
|
||||
<link href=/_/style.css rel=stylesheet>
|
||||
</head>
|
||||
HEAD;
|
||||
}
|
||||
@ -136,18 +142,19 @@ class Layout
|
||||
rel=noopener>Developed</a> and hosted by
|
||||
<a href=https://bitbadger.solutions target=_blank rel=noopener>Bit Badger Solutions</a>
|
||||
</small></em>
|
||||
<script src=https://unpkg.com/htmx.org@2.0.0 crossorigin=anonymous
|
||||
integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw"></script>
|
||||
<script>if (!htmx) document.write('<script src=\"/script/htmx.min.js\"><\/script>')</script>
|
||||
<script src=https://unpkg.com/htmx.org@2.0.4 crossorigin=anonymous
|
||||
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"></script>
|
||||
<script>if (!htmx) document.write('<script src=\"/_/htmx.min.js\"><\/script>')</script>
|
||||
<script async src=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin=anonymous></script>
|
||||
<script>
|
||||
setTimeout(function () {
|
||||
if (!bootstrap) document.write('<script src=\"/script/bootstrap.bundle.min.js\"><\/script>')
|
||||
if (!bootstrap) document.write('<script src=\"/_/bootstrap.bundle.min.js\"><\/script>')
|
||||
}, 2000)
|
||||
</script>
|
||||
<script src=/script/mpj.js></script>
|
||||
<script src=/_/mpj.js></script>
|
||||
<script src=/_/relative-date-time.js defer></script>
|
||||
</footer><?php
|
||||
}
|
||||
|
||||
|
@ -1,66 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\UI;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* A type of relative date, along with the formatting strings
|
||||
*/
|
||||
enum RelativeDate: string
|
||||
{
|
||||
case LessThanXMinutes = 'less than a minute|less than %d minutes';
|
||||
case XMinutes = 'a minute|%d minutes';
|
||||
case AboutXHours = 'about an hour|about %d hours';
|
||||
case XHours = 'an hour|%d hours';
|
||||
case XDays = 'a day|%d days';
|
||||
case AboutXWeeks = 'about a week|about %d weeks';
|
||||
case XWeeks = 'a week|%d weeks';
|
||||
case AboutXMonths = 'about a month|about %d months';
|
||||
case XMonths = 'a month|%d months';
|
||||
case AboutXYears = 'about a year|about %d years';
|
||||
case XYears = 'a year|%d years';
|
||||
case OverXYears = 'over a year|over %d years';
|
||||
case AlmostXYears = 'almost a year|almost %d years';
|
||||
|
||||
// Many thanks to date-fns (https://date-fns.org) for this logic
|
||||
/**
|
||||
* Format the distance between two dates
|
||||
*
|
||||
* @param string|DateTimeImmutable $from The starting date/time
|
||||
* @param string|DateTimeImmutable $to The ending date/time
|
||||
* @return string The distance between two dates
|
||||
* @throws Exception If date/time objects cannot be created
|
||||
*/
|
||||
public static function between(string|DateTimeImmutable $from, string|DateTimeImmutable $to): string
|
||||
{
|
||||
$aDay = 1_440.0;
|
||||
$almost2Days = 2_520.0;
|
||||
$aMonth = 43_200.0;
|
||||
$twoMonths = 86_400.0;
|
||||
|
||||
$dtFrom = is_string($from) ? new DateTimeImmutable($from) : $from;
|
||||
$dtTo = is_string($to) ? new DateTimeImmutable($to) : $to;
|
||||
$minutes = abs($dtFrom->getTimestamp() - $dtTo->getTimestamp()) / 60;
|
||||
$months = round($minutes / $aMonth);
|
||||
$years = round($months / 12);
|
||||
|
||||
[$type, $number] = match (true) {
|
||||
$minutes < 1.0 => [RelativeDate::LessThanXMinutes, 1],
|
||||
$minutes < 45.0 => [RelativeDate::XMinutes, round($minutes)],
|
||||
$minutes < 90.0 => [RelativeDate::AboutXHours, 1],
|
||||
$minutes < $aDay => [RelativeDate::AboutXHours, round($minutes / 60)],
|
||||
$minutes < $almost2Days => [RelativeDate::XDays, 1],
|
||||
$minutes < $aMonth => [RelativeDate::XDays, round($minutes / $aDay)],
|
||||
$minutes < $twoMonths => [RelativeDate::AboutXMonths, round($minutes / $aMonth)],
|
||||
$months < 12 => [RelativeDate::XMonths, round($minutes / $aMonth)],
|
||||
$months % 12 < 3 => [RelativeDate::AboutXYears, $years],
|
||||
$months % 12 < 9 => [RelativeDate::OverXYears, $years],
|
||||
default => [RelativeDate::AlmostXYears, $years]
|
||||
};
|
||||
[$singular, $plural] = explode('|', $type->value);
|
||||
$value = $number == 1 ? $singular : sprintf($plural, $number);
|
||||
return $dtFrom > $dtTo ? "$value ago" : "in $value";
|
||||
}
|
||||
}
|
1
src/public/_/htmx.min.js
vendored
Normal file
1
src/public/_/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
104
src/public/_/mpj.js
Normal file
104
src/public/_/mpj.js
Normal file
@ -0,0 +1,104 @@
|
||||
"use strict"
|
||||
|
||||
/** myPrayerJournal script */
|
||||
window.mpj = {
|
||||
/**
|
||||
* Show a message via toast
|
||||
* @param {string} message The message to show
|
||||
*/
|
||||
showToast (message) {
|
||||
const [level, msg] = message.split("|||")
|
||||
|
||||
let header
|
||||
if (level !== "success") {
|
||||
const heading = typ => `<span class="me-auto"><strong>${typ.toUpperCase()}</strong></span>`
|
||||
|
||||
header = document.createElement("div")
|
||||
header.className = "toast-header"
|
||||
header.innerHTML = heading(level === "warning" ? level : "error")
|
||||
|
||||
const close = document.createElement("button")
|
||||
close.type = "button"
|
||||
close.className = "btn-close"
|
||||
close.setAttribute("data-bs-dismiss", "toast")
|
||||
close.setAttribute("aria-label", "Close")
|
||||
header.appendChild(close)
|
||||
}
|
||||
|
||||
const body = document.createElement("div")
|
||||
body.className = "toast-body"
|
||||
body.innerText = msg
|
||||
|
||||
const toastEl = document.createElement("div")
|
||||
toastEl.className = `toast bg-${level === "error" ? "danger" : level} text-white`
|
||||
toastEl.setAttribute("role", "alert")
|
||||
toastEl.setAttribute("aria-live", "assertlive")
|
||||
toastEl.setAttribute("aria-atomic", "true")
|
||||
toastEl.addEventListener("hidden.bs.toast", e => e.target.remove())
|
||||
if (header) toastEl.appendChild(header)
|
||||
|
||||
toastEl.appendChild(body)
|
||||
document.getElementById("toasts").appendChild(toastEl)
|
||||
new bootstrap.Toast(toastEl, { autohide: level === "success" }).show()
|
||||
},
|
||||
/**
|
||||
* Load local version of Bootstrap CSS if the CDN load failed
|
||||
*/
|
||||
ensureCss () {
|
||||
let loaded = false
|
||||
for (let i = 0; !loaded && i < document.styleSheets.length; i++) {
|
||||
loaded = document.styleSheets[i].href.endsWith("bootstrap.min.css")
|
||||
}
|
||||
if (!loaded) {
|
||||
const css = document.createElement("link")
|
||||
css.rel = "stylesheet"
|
||||
css.href = "/style/bootstrap.min.css"
|
||||
document.getElementsByTagName("head")[0].appendChild(css)
|
||||
}
|
||||
},
|
||||
/** Script for the request edit component */
|
||||
edit: {
|
||||
/**
|
||||
* Toggle the recurrence input fields
|
||||
* @param {Event} e The click event
|
||||
*/
|
||||
toggleRecurrence ({ target }) {
|
||||
const isDisabled = target.value === "Immediate"
|
||||
;["recurCount", "recurInterval"].forEach(it => document.getElementById(it).disabled = isDisabled)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* The time zone of the current browser
|
||||
* @type {string}
|
||||
**/
|
||||
timeZone: undefined,
|
||||
/**
|
||||
* Derive the time zone from the current browser
|
||||
*/
|
||||
deriveTimeZone () {
|
||||
try {
|
||||
this.timeZone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone
|
||||
} catch (_) { }
|
||||
}
|
||||
}
|
||||
|
||||
htmx.on("htmx:afterOnLoad", function (evt) {
|
||||
const hdrs = evt.detail.xhr.getAllResponseHeaders()
|
||||
// Show a message if there was one in the response
|
||||
if (hdrs.indexOf("x-toast") >= 0) {
|
||||
mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
|
||||
}
|
||||
// Hide a modal window if requested
|
||||
if (hdrs.indexOf("x-hide-modal") >= 0) {
|
||||
document.getElementById(evt.detail.xhr.getResponseHeader("x-hide-modal") + "Dismiss").click()
|
||||
}
|
||||
})
|
||||
|
||||
htmx.on("htmx:configRequest", function (evt) {
|
||||
// Send the user's current time zone so that we can display local time
|
||||
if (mpj.timeZone) {
|
||||
evt.detail.headers["X-Time-Zone"] = mpj.timeZone
|
||||
}
|
||||
})
|
||||
|
||||
mpj.deriveTimeZone()
|
121
src/public/_/relative-date-time.js
Normal file
121
src/public/_/relative-date-time.js
Normal file
@ -0,0 +1,121 @@
|
||||
"use strict"
|
||||
|
||||
/**
|
||||
* Relative Date/Time Custom HTML Element
|
||||
*
|
||||
* This creates an element that will take the existing date/time and replace it with words (ex. "about a year ago",
|
||||
* "in 3 hours"). It will update based on the interval provided in the tag.
|
||||
*
|
||||
* ```html
|
||||
* <relative-date-time interval=5000>2024-08-22T12:34:56+00:00</relative-date-time>
|
||||
* ```
|
||||
*/
|
||||
class RelativeDateTime extends HTMLElement {
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* The date, parsed from the `innerHTML` of the element
|
||||
* @type Date
|
||||
*/
|
||||
#jsDate
|
||||
|
||||
/**
|
||||
* The ID of the interval (set via `setTimeout`, passed to `clearTimeout`)
|
||||
* @type ?number
|
||||
*/
|
||||
#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()
|
||||
this.#timeOut = setInterval(() => this.#update(), parseInt(this.getAttribute("interval")))
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.#timeOut) clearInterval(this.#timeOut)
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("relative-date-time", RelativeDateTime)
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
use MyPrayerJournal\UI\Component;
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\UI\Layout;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\UI\{Component, Layout};
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\UI\Layout;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\UI\Layout;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\UI\Component;use MyPrayerJournal\UI\Layout;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
use MyPrayerJournal\UI\{Component, Layout};
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\UI\Layout;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\UI\{Component, Layout};
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use BitBadger\PDODocument\RemoveFields;
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
use MyPrayerJournal\Domain\{RecurrencePeriod, Request, RequestAction};
|
||||
|
@ -1,7 +1,13 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Domain\{History, Note, RequestAction};
|
||||
use MyPrayerJournal\UI\{Layout, RelativeDate};
|
||||
use MyPrayerJournal\UI\{Component, Layout};
|
||||
|
||||
require '../../start.php';
|
||||
|
||||
@ -28,7 +34,7 @@ Layout::pageHead('Full Request');?>
|
||||
<h6 class="card-subtitle text-muted mb-2"><?php
|
||||
if (!is_null($answered)) { ?>
|
||||
Answered <?=$answered->format('F j, Y')?>
|
||||
(<?=RelativeDate::between('now', $req->history[0]->asOf);?>) •<?php
|
||||
(<?=Component::relativeDate($req->history[0]->asOf)?>) •<?php
|
||||
} ?>
|
||||
Prayed <?=number_format($prayed)?> times • Open <?=number_format($daysOpen)?> days
|
||||
</h6>
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use BitBadger\PDODocument\Patch;
|
||||
use MyPrayerJournal\Table;
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use BitBadger\PDODocument\Patch;
|
||||
use MyPrayerJournal\Table;
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use BitBadger\PDODocument\{Document, Patch, RemoveFields};
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use BitBadger\PDODocument\RemoveFields;
|
||||
use MyPrayerJournal\Table;
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use BitBadger\PDODocument\Patch;
|
||||
use MyPrayerJournal\Table;
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
use MyPrayerJournal\Domain\Request;
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
use MyPrayerJournal\Domain\Request;
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
use MyPrayerJournal\Domain\Request;
|
||||
|
1
src/public/script/htmx.min.js
vendored
1
src/public/script/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,213 +0,0 @@
|
||||
"use strict"
|
||||
|
||||
/** myPrayerJournal script */
|
||||
window.mpj = {
|
||||
/**
|
||||
* Show a message via toast
|
||||
* @param {string} message The message to show
|
||||
*/
|
||||
showToast (message) {
|
||||
const [level, msg] = message.split("|||")
|
||||
|
||||
let header
|
||||
if (level !== "success") {
|
||||
const heading = typ => `<span class="me-auto"><strong>${typ.toUpperCase()}</strong></span>`
|
||||
|
||||
header = document.createElement("div")
|
||||
header.className = "toast-header"
|
||||
header.innerHTML = heading(level === "warning" ? level : "error")
|
||||
|
||||
const close = document.createElement("button")
|
||||
close.type = "button"
|
||||
close.className = "btn-close"
|
||||
close.setAttribute("data-bs-dismiss", "toast")
|
||||
close.setAttribute("aria-label", "Close")
|
||||
header.appendChild(close)
|
||||
}
|
||||
|
||||
const body = document.createElement("div")
|
||||
body.className = "toast-body"
|
||||
body.innerText = msg
|
||||
|
||||
const toastEl = document.createElement("div")
|
||||
toastEl.className = `toast bg-${level === "error" ? "danger" : level} text-white`
|
||||
toastEl.setAttribute("role", "alert")
|
||||
toastEl.setAttribute("aria-live", "assertlive")
|
||||
toastEl.setAttribute("aria-atomic", "true")
|
||||
toastEl.addEventListener("hidden.bs.toast", e => e.target.remove())
|
||||
if (header) toastEl.appendChild(header)
|
||||
|
||||
toastEl.appendChild(body)
|
||||
document.getElementById("toasts").appendChild(toastEl)
|
||||
new bootstrap.Toast(toastEl, { autohide: level === "success" }).show()
|
||||
},
|
||||
/**
|
||||
* Load local version of Bootstrap CSS if the CDN load failed
|
||||
*/
|
||||
ensureCss () {
|
||||
let loaded = false
|
||||
for (let i = 0; !loaded && i < document.styleSheets.length; i++) {
|
||||
loaded = document.styleSheets[i].href.endsWith("bootstrap.min.css")
|
||||
}
|
||||
if (!loaded) {
|
||||
const css = document.createElement("link")
|
||||
css.rel = "stylesheet"
|
||||
css.href = "/style/bootstrap.min.css"
|
||||
document.getElementsByTagName("head")[0].appendChild(css)
|
||||
}
|
||||
},
|
||||
/** Script for the request edit component */
|
||||
edit: {
|
||||
/**
|
||||
* Toggle the recurrence input fields
|
||||
* @param {Event} e The click event
|
||||
*/
|
||||
toggleRecurrence ({ target }) {
|
||||
const isDisabled = target.value === "Immediate"
|
||||
;["recurCount", "recurInterval"].forEach(it => document.getElementById(it).disabled = isDisabled)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* The time zone of the current browser
|
||||
* @type {string}
|
||||
**/
|
||||
timeZone: undefined,
|
||||
/**
|
||||
* Derive the time zone from the current browser
|
||||
*/
|
||||
deriveTimeZone () {
|
||||
try {
|
||||
this.timeZone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone
|
||||
} catch (_) { }
|
||||
}
|
||||
}
|
||||
|
||||
htmx.on("htmx:afterOnLoad", function (evt) {
|
||||
const hdrs = evt.detail.xhr.getAllResponseHeaders()
|
||||
// Show a message if there was one in the response
|
||||
if (hdrs.indexOf("x-toast") >= 0) {
|
||||
mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
|
||||
}
|
||||
// Hide a modal window if requested
|
||||
if (hdrs.indexOf("x-hide-modal") >= 0) {
|
||||
document.getElementById(evt.detail.xhr.getResponseHeader("x-hide-modal") + "Dismiss").click()
|
||||
}
|
||||
})
|
||||
|
||||
htmx.on("htmx:configRequest", function (evt) {
|
||||
// Send the user's current time zone so that we can display local time
|
||||
if (mpj.timeZone) {
|
||||
evt.detail.headers["X-Time-Zone"] = mpj.timeZone
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Auth0\SDK\Exception\ConfigurationException;
|
||||
use BitBadger\PDODocument\{AutoId, Configuration, Definition, DocumentException};
|
||||
@ -72,8 +78,7 @@ function hide_modal(string $name): void
|
||||
* @param array $methods The allowable HTTP methods
|
||||
* @param bool $redirect Whether to redirect not-logged-on users (optional, defaults to true)
|
||||
* @return Request The request (failures will not return)
|
||||
* @throws ConfigurationException If any is encountered
|
||||
* @throws DocumentException If any is encountered
|
||||
* @throws ConfigurationException|DocumentException If any is encountered
|
||||
*/
|
||||
function validate_request(string $id, array $methods, bool $redirect = true): Request
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user