Add TZ handling/relative date
This commit is contained in:
parent
817d7876db
commit
a5727a84fc
107
src/app/Dates.php
Normal file
107
src/app/Dates.php
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MyPrayerJournal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The different distance formats supported
|
||||||
|
*/
|
||||||
|
enum DistanceFormat
|
||||||
|
{
|
||||||
|
case LessThanXMinutes;
|
||||||
|
case XMinutes;
|
||||||
|
case AboutXHours;
|
||||||
|
case XHours;
|
||||||
|
case XDays;
|
||||||
|
case AboutXWeeks;
|
||||||
|
case XWeeks;
|
||||||
|
case AboutXMonths;
|
||||||
|
case XMonths;
|
||||||
|
case AboutXYears;
|
||||||
|
case XYears;
|
||||||
|
case OverXYears;
|
||||||
|
case AlmostXYears;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the formatting string for the given format and number
|
||||||
|
*
|
||||||
|
* @param DistanceFormat $it The distance format
|
||||||
|
* @param bool $singular If true, returns the singular version; if false (default), returns the plural version
|
||||||
|
* @return string The format string
|
||||||
|
*/
|
||||||
|
public static function format(DistanceFormat $it, bool $singular = false): string
|
||||||
|
{
|
||||||
|
return match ($it) {
|
||||||
|
DistanceFormat::LessThanXMinutes => $singular ? 'less than a minute' : 'less than %i minutes',
|
||||||
|
DistanceFormat::XMinutes => $singular ? 'a minute' : '%i minutes',
|
||||||
|
DistanceFormat::AboutXHours => $singular ? 'about an hour' : 'about %i hours',
|
||||||
|
DistanceFormat::XHours => $singular ? 'an hour' : '%i hours',
|
||||||
|
DistanceFormat::XDays => $singular ? 'a day' : '%i days',
|
||||||
|
DistanceFormat::AboutXWeeks => $singular ? 'about a week' : 'about %i weeks',
|
||||||
|
DistanceFormat::XWeeks => $singular ? 'a week' : '%i weeks',
|
||||||
|
DistanceFormat::AboutXMonths => $singular ? 'about a month' : 'about %i months',
|
||||||
|
DistanceFormat::XMonths => $singular ? 'a month' : '%i months',
|
||||||
|
DistanceFormat::AboutXYears => $singular ? 'about a year' : 'about %i years',
|
||||||
|
DistanceFormat::XYears => $singular ? 'a year' : '%i years',
|
||||||
|
DistanceFormat::OverXYears => $singular ? 'over a year' : 'over %i years',
|
||||||
|
DistanceFormat::AlmostXYears => $singular ? 'almost a year' : 'almost %i years',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dates
|
||||||
|
{
|
||||||
|
/** Minutes in a day */
|
||||||
|
private const A_DAY = 1_440;
|
||||||
|
|
||||||
|
/** Minutes in two days(-ish) */
|
||||||
|
private const ALMOST_2_DAYS = 2_520;
|
||||||
|
|
||||||
|
/** Minutes in a month */
|
||||||
|
private const A_MONTH = 43_200;
|
||||||
|
|
||||||
|
/** Minutes in two months */
|
||||||
|
private const TWO_MONTHS = 86_400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a UTC-referenced current date/time
|
||||||
|
*
|
||||||
|
* @return \DateTimeImmutable The current date/time with UTC reference
|
||||||
|
*/
|
||||||
|
public static function now(): \DateTimeImmutable
|
||||||
|
{
|
||||||
|
return new \DateTimeImmutable(timezone: new \DateTimeZone('Etc/UTC'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the distance between two instants in approximate English terms
|
||||||
|
*
|
||||||
|
* @param \DateTimeInterface $startOn The starting date/time for the comparison
|
||||||
|
* @param \DateTimeInterface $endOn THe ending date/time for the comparison
|
||||||
|
* @return string The formatted interval
|
||||||
|
*/
|
||||||
|
public static function formatDistance(\DateTimeInterface $startOn, \DateTimeInterface $endOn): string
|
||||||
|
{
|
||||||
|
$diff = $startOn->diff($endOn);
|
||||||
|
$minutes =
|
||||||
|
$diff->i + ($diff->h * 60) + ($diff->d * 60 * 24) + ($diff->m * 60 * 24 * 30) + ($diff->y * 60 * 24 * 365);
|
||||||
|
$months = round($minutes / self::A_MONTH);
|
||||||
|
$years = $months / 12;
|
||||||
|
[ $format, $number ] = match (true) {
|
||||||
|
$minutes < 1 => [ DistanceFormat::LessThanXMinutes, 1 ],
|
||||||
|
$minutes < 45 => [ DistanceFormat::XMinutes, $minutes ],
|
||||||
|
$minutes < 90 => [ DistanceFormat::AboutXHours, 1 ],
|
||||||
|
$minutes < self::A_DAY => [ DistanceFormat::AboutXHours, round($minutes / 60) ],
|
||||||
|
$minutes < self::ALMOST_2_DAYS => [ DistanceFormat::XDays, 1 ],
|
||||||
|
$minutes < self::A_MONTH => [ DistanceFormat::XDays, round($minutes / self::A_DAY) ],
|
||||||
|
$minutes < self::TWO_MONTHS => [ DistanceFormat::AboutXMonths, round($minutes / self::A_MONTH) ],
|
||||||
|
$months < 12 => [ DistanceFormat::XMonths, round($minutes / self::A_MONTH) ],
|
||||||
|
$months % 12 < 3 => [ DistanceFormat::AboutXYears, $years ],
|
||||||
|
$months % 12 < 9 => [ DistanceFormat::OverXYears, $years ],
|
||||||
|
default => [ DistanceFormat::AlmostXYears, $years + 1 ],
|
||||||
|
};
|
||||||
|
|
||||||
|
$relativeWords = sprintf(DistanceFormat::format($format, $number == 1), $number);
|
||||||
|
return $startOn > $endOn ? "$relativeWords ago" : "in $relativeWords";
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,16 @@ app()->group('/user', function () {
|
||||||
app()->get('/log-off', AppUser::logOff(...));
|
app()->get('/log-off', AppUser::logOff(...));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Extract the user's time zone from the request, if present
|
||||||
|
app()->use(new class extends \Leaf\Middleware {
|
||||||
|
public function call()
|
||||||
|
{
|
||||||
|
$_REQUEST['USER_TIME_ZONE'] = new \DateTimeZone(
|
||||||
|
array_key_exists('HTTP_X_TIME_ZONE', $_SERVER) ? $_SERVER['HTTP_X_TIME_ZONE'] : 'Etc/UTC');
|
||||||
|
$this->next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: remove before go-live
|
// TODO: remove before go-live
|
||||||
$stdOut = fopen('php://stdout', 'w');
|
$stdOut = fopen('php://stdout', 'w');
|
||||||
function stdout(string $msg)
|
function stdout(string $msg)
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
<?php
|
<?php
|
||||||
$spacer = '<span> </span>'; ?>
|
use MyPrayerJournal\Dates;
|
||||||
|
|
||||||
|
$spacer = '<span> </span>';
|
||||||
|
/**
|
||||||
|
* Format the activity and relative time
|
||||||
|
*
|
||||||
|
* @param string $activity The activity performed (activity or prayed)
|
||||||
|
* @param \DateTimeImmutable $asOf The date/time the activity was performed
|
||||||
|
*/
|
||||||
|
function formatActivity(string $activity, \DateTimeImmutable $asOf)
|
||||||
|
{
|
||||||
|
echo "last $activity <span title=\"" . $asOf->setTimezone($_REQUEST['USER_TIME_ZONE'])->format('l, F jS, Y/g:ia T')
|
||||||
|
. '">' . Dates::formatDistance(Dates::now(), $asOf) . '</span>';
|
||||||
|
} ?>
|
||||||
<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="tool-bar">
|
<div class="card-header p-0 d-flex" role="tool-bar">
|
||||||
|
@ -27,13 +40,10 @@ $spacer = '<span> </span>'; ?>
|
||||||
</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><?php
|
||||||
// TODO: relative time, time zone handling, etc.
|
[ $activity, $asOf ] = is_null($request->lastPrayed)
|
||||||
if (is_null($request->lastPrayed)) {
|
? [ 'activity', $request->asOf ]
|
||||||
echo "last activity {$request->asOf}";
|
: [ 'prayed', $request->lastPrayed ];
|
||||||
|
formatActivity($activity, $asOf); ?>
|
||||||
} else {
|
|
||||||
echo "last prayed {$request->lastPrayed}";
|
|
||||||
} ?>
|
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<span class="material-icons">add_box</span> Add a Prayer Request
|
<span class="material-icons">add_box</span> Add a Prayer Request
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p hx-get="/components/journal-items" hx-swap="outerHTML" hx-trigger="load">
|
<p hx-get="/components/journal-items" hx-swap="outerHTML" hx-trigger="load delay:.25s">
|
||||||
Loading your prayer journal…
|
Loading your prayer journal…
|
||||||
</p>
|
</p>
|
||||||
<div id="notesModal" class="modal fade" tabindex="-1" aria-labelled-by="nodesModalLabel" aria-hidden="true">
|
<div id="notesModal" class="modal fade" tabindex="-1" aria-labelled-by="nodesModalLabel" aria-hidden="true">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user