Refine relative date/time; add docs to files

This commit is contained in:
Daniel J. Summers 2024-12-14 12:23:11 -05:00
parent 516a903565
commit 75680fae00
42 changed files with 475 additions and 325 deletions

View File

@ -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; namespace MyPrayerJournal;

View File

@ -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; namespace MyPrayerJournal\Domain;

View File

@ -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; namespace MyPrayerJournal\Domain;

View File

@ -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; namespace MyPrayerJournal\Domain;

View File

@ -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; namespace MyPrayerJournal\Domain;

View File

@ -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; namespace MyPrayerJournal\Domain;

View File

@ -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; namespace MyPrayerJournal\Domain;

View File

@ -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; namespace MyPrayerJournal;

View File

@ -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; namespace MyPrayerJournal\UI;
@ -117,7 +123,7 @@ class Component
$parsed = new DateTimeImmutable($date); $parsed = new DateTimeImmutable($date);
$iso = $parsed->format('c'); $iso = $parsed->format('c');
$title = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone']))->format('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');
return "<relative-date-time title=\"$title\" interval=5000>$iso</relative-date-time>"; return "<relative-date-time title=\"$title\" interval=10000>$iso</relative-date-time>";
} }
/** /**

View File

@ -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; namespace MyPrayerJournal\UI;
@ -56,7 +62,7 @@ class Layout
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin=anonymous> crossorigin=anonymous>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel=stylesheet> <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>
HEAD; HEAD;
} }
@ -136,18 +142,19 @@ class Layout
rel=noopener>Developed</a> and hosted by rel=noopener>Developed</a> and hosted by
<a href=https://bitbadger.solutions target=_blank rel=noopener>Bit Badger Solutions</a> <a href=https://bitbadger.solutions target=_blank rel=noopener>Bit Badger Solutions</a>
</small></em> </small></em>
<script src=https://unpkg.com/htmx.org@2.0.0 crossorigin=anonymous <script src=https://unpkg.com/htmx.org@2.0.4 crossorigin=anonymous
integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw"></script> integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"></script>
<script>if (!htmx) document.write('<script src=\"/script/htmx.min.js\"><\/script>')</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 <script async src=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin=anonymous></script> crossorigin=anonymous></script>
<script> <script>
setTimeout(function () { 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) }, 2000)
</script> </script>
<script src=/script/mpj.js></script> <script src=/_/mpj.js></script>
<script src=/_/relative-date-time.js defer></script>
</footer><?php </footer><?php
} }

View File

@ -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

File diff suppressed because one or more lines are too long

104
src/public/_/mpj.js Normal file
View 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()

View 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)

View File

@ -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\Auth;
use MyPrayerJournal\UI\Component; use MyPrayerJournal\UI\Component;

View File

@ -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; use MyPrayerJournal\UI\Layout;

View File

@ -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}; use MyPrayerJournal\UI\{Component, Layout};

View File

@ -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; use MyPrayerJournal\UI\Layout;

View File

@ -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; use MyPrayerJournal\UI\Layout;

View File

@ -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; use MyPrayerJournal\UI\Component;use MyPrayerJournal\UI\Layout;

View File

@ -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\Auth;
use MyPrayerJournal\UI\{Component, Layout}; use MyPrayerJournal\UI\{Component, Layout};

View File

@ -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; use MyPrayerJournal\UI\Layout;

View File

@ -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}; use MyPrayerJournal\UI\{Component, Layout};

View File

@ -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\InspiredByFSharp\Option;
use BitBadger\PDODocument\RemoveFields; use BitBadger\PDODocument\RemoveFields;

View File

@ -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\Auth;
use MyPrayerJournal\Domain\{RecurrencePeriod, Request, RequestAction}; use MyPrayerJournal\Domain\{RecurrencePeriod, Request, RequestAction};

View File

@ -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\Domain\{History, Note, RequestAction};
use MyPrayerJournal\UI\{Layout, RelativeDate}; use MyPrayerJournal\UI\{Component, Layout};
require '../../start.php'; require '../../start.php';
@ -28,7 +34,7 @@ Layout::pageHead('Full Request');?>
<h6 class="card-subtitle text-muted mb-2"><?php <h6 class="card-subtitle text-muted mb-2"><?php
if (!is_null($answered)) { ?> if (!is_null($answered)) { ?>
Answered <?=$answered->format('F j, Y')?> Answered <?=$answered->format('F j, Y')?>
(<?=RelativeDate::between('now', $req->history[0]->asOf);?>) &bull;<?php (<?=Component::relativeDate($req->history[0]->asOf)?>) &bull;<?php
} ?> } ?>
Prayed <?=number_format($prayed)?> times &bull; Open <?=number_format($daysOpen)?> days Prayed <?=number_format($prayed)?> times &bull; Open <?=number_format($daysOpen)?> days
</h6> </h6>

View File

@ -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 BitBadger\PDODocument\Patch;
use MyPrayerJournal\Table; use MyPrayerJournal\Table;

View File

@ -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 BitBadger\PDODocument\Patch;
use MyPrayerJournal\Table; use MyPrayerJournal\Table;

View File

@ -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\InspiredByFSharp\Option;
use BitBadger\PDODocument\{Document, Patch, RemoveFields}; use BitBadger\PDODocument\{Document, Patch, RemoveFields};

View File

@ -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 BitBadger\PDODocument\RemoveFields;
use MyPrayerJournal\Table; use MyPrayerJournal\Table;

View File

@ -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 BitBadger\PDODocument\Patch;
use MyPrayerJournal\Table; use MyPrayerJournal\Table;

View File

@ -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\Auth;
use MyPrayerJournal\Domain\Request; use MyPrayerJournal\Domain\Request;

View File

@ -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\Auth;
use MyPrayerJournal\Domain\Request; use MyPrayerJournal\Domain\Request;

View File

@ -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\Auth;
use MyPrayerJournal\Domain\Request; use MyPrayerJournal\Domain\Request;

File diff suppressed because one or more lines are too long

View File

@ -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)

View File

@ -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\Auth;

View File

@ -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\Auth;

View File

@ -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\Auth;

View File

@ -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 Auth0\SDK\Exception\ConfigurationException;
use BitBadger\PDODocument\{AutoId, Configuration, Definition, DocumentException}; 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 array $methods The allowable HTTP methods
* @param bool $redirect Whether to redirect not-logged-on users (optional, defaults to true) * @param bool $redirect Whether to redirect not-logged-on users (optional, defaults to true)
* @return Request The request (failures will not return) * @return Request The request (failures will not return)
* @throws ConfigurationException If any is encountered * @throws ConfigurationException|DocumentException If any is encountered
* @throws DocumentException If any is encountered
*/ */
function validate_request(string $id, array $methods, bool $redirect = true): Request function validate_request(string $id, array $methods, bool $redirect = true): Request
{ {