diff --git a/src/MyPrayerJournal/Handlers.fs b/src/MyPrayerJournal/Handlers.fs
index 4d0d9f5..18a90b1 100644
--- a/src/MyPrayerJournal/Handlers.fs
+++ b/src/MyPrayerJournal/Handlers.fs
@@ -551,9 +551,9 @@ let routes = [
]
PATCH [
route "" Request.update // done
- routef "/%s/cancel-snooze" Request.cancelSnooze
+ routef "/%s/cancel-snooze" Request.cancelSnooze // done
routef "/%s/prayed" Request.prayed // done
- routef "/%s/show" Request.show
+ routef "/%s/show" Request.show // done
routef "/%s/snooze" Request.snooze // done
]
POST [
diff --git a/src/composer.json b/src/composer.json
index 6aad2e7..cc8af6b 100644
--- a/src/composer.json
+++ b/src/composer.json
@@ -15,7 +15,9 @@
},
"autoload": {
"psr-4": {
- "MyPrayerJournal\\": "lib/"
+ "MyPrayerJournal\\": "lib/",
+ "MyPrayerJournal\\Domain\\": "lib/Domain",
+ "MyPrayerJournal\\UI\\": "lib/UI"
}
},
"config": {
diff --git a/src/convert-from-v3.php b/src/convert-from-v3.php
index 03b3407..3ced31c 100644
--- a/src/convert-from-v3.php
+++ b/src/convert-from-v3.php
@@ -2,7 +2,8 @@
use BitBadger\PDODocument\{Configuration, Custom, Definition, Document, Mode};
use BitBadger\PDODocument\Mapper\ArrayMapper;
-use MyPrayerJournal\{History, Note, Recurrence, RecurrencePeriod, Request, RequestAction, Table};
+use MyPrayerJournal\Domain\{History, Note, Recurrence, RecurrencePeriod, Request, RequestAction};
+use MyPrayerJournal\Table;
require 'start.php';
diff --git a/src/lib/Auth.php b/src/lib/Auth.php
index d667aad..2d7b2a3 100644
--- a/src/lib/Auth.php
+++ b/src/lib/Auth.php
@@ -5,10 +5,19 @@ namespace MyPrayerJournal;
use Auth0\SDK\Auth0;
use Auth0\SDK\Exception\ConfigurationException;
+/**
+ * myPrayerJournal-specific authorization functions
+ */
class Auth
{
+ /** @var Auth0|null The Auth0 client to use for requests (initialized on first use) */
private static ?Auth0 $auth0 = null;
+ /**
+ * Get the current Auth0 client
+ *
+ * @return Auth0 The current Auth0 client
+ */
public static function client(): Auth0
{
if (is_null(self::$auth0)) {
diff --git a/src/lib/History.php b/src/lib/Domain/History.php
similarity index 95%
rename from src/lib/History.php
rename to src/lib/Domain/History.php
index d555e96..27eea02 100644
--- a/src/lib/History.php
+++ b/src/lib/Domain/History.php
@@ -1,6 +1,6 @@
$name";
- }
-
- /**
- * Render the journal items for the current user
- *
- * @throws DocumentException If any is encountered
- */
- public static function journal(): void
- {
- Layout::bareHead();
- $reqs = Request::forJournal();
- if ($reqs->hasItems()) { ?>
- ';
- foreach ($reqs->items() as /** @var Request $req */ $req) { ?>
-
-
-
-
-
=htmlentities($req->currentText());?>
-
-
-
-
-
-
-
-
-
=$text?>
'btn btn-primary']); ?>
-
-
- $value) echo " $key=\"" . htmlspecialchars($value) . "\""; ?>>=$text?> setTimezone(new DateTimeZone($_REQUEST['time_zone']));
- echo ''
- . self::formatDistance('now', $parsed) . ' ';
- }
-
- // 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 formatDistance(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);
-
- $typeAndNumber = match (true) {
- $minutes < 1.0 => [FormatDistanceToken::LessThanXMinutes, 1],
- $minutes < 45.0 => [FormatDistanceToken::XMinutes, round($minutes)],
- $minutes < 90.0 => [FormatDistanceToken::AboutXHours, 1],
- $minutes < $aDay => [FormatDistanceToken::AboutXHours, round($minutes / 60)],
- $minutes < $almost2Days => [FormatDistanceToken::XDays, 1],
- $minutes < $aMonth => [FormatDistanceToken::XDays, round($minutes / $aDay)],
- $minutes < $twoMonths => [FormatDistanceToken::AboutXMonths, round($minutes / $aMonth)],
- $months < 12 => [FormatDistanceToken::XMonths, round($minutes / $aMonth)],
- $months % 12 < 3 => [FormatDistanceToken::AboutXYears, $years],
- $months % 12 < 9 => [FormatDistanceToken::OverXYears, $years],
- default => [FormatDistanceToken::AlmostXYears, $years]
- };
- $format = match ($typeAndNumber[0]) {
- FormatDistanceToken::LessThanXMinutes => ['less than a minute', 'less than %d minutes'],
- FormatDistanceToken::XMinutes => ['a minute', '%d minutes'],
- FormatDistanceToken::AboutXHours => ['about an hour', 'about %d hours'],
- FormatDistanceToken::XHours => ['an hour', '%d hours'],
- FormatDistanceToken::XDays => ['a day', '%d days'],
- FormatDistanceToken::AboutXWeeks => ['about a week', 'about %d weeks'],
- FormatDistanceToken::XWeeks => ['a week', '%d weeks'],
- FormatDistanceToken::AboutXMonths => ['about a month', 'about %d months'],
- FormatDistanceToken::XMonths => ['a month', '%d months'],
- FormatDistanceToken::AboutXYears => ['about a year', 'about %d years'],
- FormatDistanceToken::XYears => ['a year', '%d years'],
- FormatDistanceToken::OverXYears => ['over a year', 'over %d years'],
- FormatDistanceToken::AlmostXYears => ['almost a year', 'almost %d years']
- };
- $value = $typeAndNumber[1] == 1 ? $format[0] : sprintf($format[1], $typeAndNumber[1]);
- return $dtFrom > $dtTo ? "$value ago" : "in $value";
- }
-
- public static function requestItem(Request $req): void
- {
- $btnClass = "btn btn-light mx-2";
- $restoreBtn = fn(string $id, string $link, string $title) =>
- '' . self::icon('restore') . ' '; ?>
- id", self::icon('description'),
- ['class' => $btnClass, 'title' => 'View Full Request']);
- if (!$req->isAnswered()) {
- self::pageLink("/request/edit?id=$req->id", self::icon('edit'),
- ['class' => $btnClass, 'title' => 'Edit Request']);
- }
- if ($req->isSnoozed()) {
- echo $restoreBtn($req->id, 'cancel-snooze', 'Cancel Snooze');
- } elseif ($req->isPending()) {
- echo $restoreBtn($req->id, 'show', 'Show Now');
- }
- echo '
' . $req->currentText();
- if ($req->isSnoozed() || $req->isPending() || $req->isAnswered()) { ?>
-
- isSnoozed():
- echo 'Snooze expires '; self::relativeDate($req->snoozedUntil);
- break;
- case $req->isPending():
- echo 'Request appears next '; self::relativeDate($req->showAfter);
- break;
- default:
- echo 'Answered '; self::relativeDate($req->history[0]->asOf);
- } ?>
-
-
$reqs The list of requests to render
- * @throws Exception If date/time instances are not valid
- */
- public static function requestList(DocumentList $reqs): void
- {
- echo '';
- foreach ($reqs->items() as $req) self::requestItem($req);
- echo '
';
- }
-}
-
-enum FormatDistanceToken
-{
- 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;
-}
diff --git a/src/lib/UI/Component.php b/src/lib/UI/Component.php
new file mode 100644
index 0000000..cecb247
--- /dev/null
+++ b/src/lib/UI/Component.php
@@ -0,0 +1,175 @@
+$name";
+ }
+
+ /**
+ * Render the journal items for the current user
+ *
+ * @throws DocumentException If any is encountered
+ */
+ public static function journal(): void
+ {
+ Layout::bareHead();
+ $reqs = Request::forJournal();
+ if ($reqs->hasItems()) { ?>
+ ';
+ foreach ($reqs->items() as /** @var Request $req */ $req) { ?>
+
+
+
+
+
=htmlentities($req->currentText());?>
+
+
+
+
+
+
+
+
+
=$text?>
+ =self::pageLink($link, $buttonText, ['class' => 'btn btn-primary'])?>
+
+
$acc . sprintf(' %s="%s"', $key, $attrs[$key]), '');
+ return sprintf('%s ', $href,
+ $href, $extraAttrs, $text);
+ }
+
+ /**
+ * Create a relative date with a tooltip with the actual date/time
+ *
+ * @return string The HTML for the relative date
+ * @throws Exception If the date/time cannot be parsed
+ */
+ public static function relativeDate(string $date): string
+ {
+ $parsed = new DateTimeImmutable($date);
+ $inZone = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone']));
+ return sprintf('%s ', date_format($inZone, 'l, F j, Y \a\t g:ia T'),
+ RelativeDate::between('now', $parsed));
+ }
+
+ /**
+ * Render a request list item
+ *
+ * @param Request $req The request on which the list items should be based
+ * @throws Exception If date/time values cannot be parsed
+ */
+ public static function requestItem(Request $req): void
+ {
+ $btnClass = "btn btn-light mx-2";
+ $restoreBtn = fn(string $id, string $link, string $title) => sprintf(
+ '%s ',
+ $btnClass, $link, $id, htmlspecialchars($title), $id, self::icon('restore')); ?>
+ id?>>id", self::icon('description'),
+ ['class' => $btnClass, 'title' => 'View Full Request']);
+ if (!$req->isAnswered()) {
+ echo self::pageLink("/request/edit?id=$req->id", self::icon('edit'),
+ ['class' => $btnClass, 'title' => 'Edit Request']);
+ }
+ if ($req->isSnoozed()) {
+ echo $restoreBtn($req->id, 'cancel-snooze', 'Cancel Snooze');
+ } elseif ($req->isPending()) {
+ echo $restoreBtn($req->id, 'show', 'Show Now');
+ }
+ echo '
' . $req->currentText();
+ if ($req->isSnoozed() || $req->isPending() || $req->isAnswered()) { ?>
+
+ isSnoozed()) {
+ echo 'Snooze expires ' . self::relativeDate($req->snoozedUntil);
+ } elseif ($req->isPending()) {
+ echo 'Request appears next ' . self::relativeDate($req->showAfter);
+ } else {
+ echo 'Answered ' . self::relativeDate($req->history[0]->asOf);
+ } ?>
+
+
$reqs The list of requests to render
+ * @throws Exception If date/time instances are not valid
+ */
+ public static function requestList(DocumentList $reqs): void
+ {
+ echo '';
+ foreach ($reqs->items() as $req) self::requestItem($req);
+ echo '
';
+ }
+}
diff --git a/src/lib/Layout.php b/src/lib/UI/Layout.php
similarity index 91%
rename from src/lib/Layout.php
rename to src/lib/UI/Layout.php
index 11abe8a..b25e64c 100644
--- a/src/lib/Layout.php
+++ b/src/lib/UI/Layout.php
@@ -1,9 +1,10 @@
['class' => 'is-active-route'],
default => []
};
- echo '';
- UI::pageLink($url, $text, $classAttr);
+ echo ' ' . Component::pageLink($url, $text, $classAttr);
}
/**
@@ -85,9 +85,10 @@ class Layout
SQL, [':userId' => $_SESSION['user_id']], new ExistsMapper())
: false; ?>
- my
Prayer Journal ',
- ['class' => 'navbar-brand']); ?>
+
+ =Component::pageLink('/',
+ '
my Prayer Journal ',
+ ['class' => 'navbar-brand'])?>
myPrayerJournal =self::displayVersion();?>
-
+
+ =Component::pageLink('/legal/privacy-policy', 'Privacy Policy')?> •
+ =Component::pageLink('/legal/terms-of-service', 'Terms of Service')?> •
Developed and hosted by
Bit Badger Solutions
diff --git a/src/lib/UI/RelativeDate.php b/src/lib/UI/RelativeDate.php
new file mode 100644
index 0000000..f8a4228
--- /dev/null
+++ b/src/lib/UI/RelativeDate.php
@@ -0,0 +1,66 @@
+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";
+ }
+}
diff --git a/src/public/components/journal-items.php b/src/public/components/journal-items.php
index 0e4de58..3cfd6b8 100644
--- a/src/public/components/journal-items.php
+++ b/src/public/components/journal-items.php
@@ -1,10 +1,11 @@
'GET') not_found();
require '../../start.php';
Auth::requireUser(false);
-UI::journal();
+Component::journal();
diff --git a/src/public/components/request/add-note.php b/src/public/components/request/add-note.php
index 5783966..73ce77f 100644
--- a/src/public/components/request/add-note.php
+++ b/src/public/components/request/add-note.php
@@ -1,6 +1,6 @@
Prior Notes for This Request notes) > 0) {
foreach ($req->notes as $note) { ?>
-
asOf); ?>
+
=Component::relativeDate($note->asOf)?>
=htmlentities($note->text)?>
diff --git a/src/public/components/request/snooze.php b/src/public/components/request/snooze.php
index 46257ea..f7dacf5 100644
--- a/src/public/components/request/snooze.php
+++ b/src/public/components/request/snooze.php
@@ -1,6 +1,6 @@
'GET') not_found();
diff --git a/src/public/index.php b/src/public/index.php
index 8af37c2..d5ed0c2 100644
--- a/src/public/index.php
+++ b/src/public/index.php
@@ -1,6 +1,6 @@
'GET') not_found();
diff --git a/src/public/journal.php b/src/public/journal.php
index f206f5a..0aebf32 100644
--- a/src/public/journal.php
+++ b/src/public/journal.php
@@ -1,6 +1,7 @@
'GET') not_found();
@@ -12,9 +13,9 @@ $name = $user['given_name'] ?? 'Your';
Layout::pageHead('Journal'); ?>
=$name?>=$name == 'Your' ? '' : '’s'?> Prayer Journal
- 'btn btn-primary']); ?>
+
+ =Component::pageLink('/request/edit?id=new', Component::icon('add_box') . ' Add a Prayer Request',
+ ['class' => 'btn btn-primary'])?>
Loading your prayer journal…
diff --git a/src/public/legal/privacy-policy.php b/src/public/legal/privacy-policy.php
index 49f133e..e763914 100644
--- a/src/public/legal/privacy-policy.php
+++ b/src/public/legal/privacy-policy.php
@@ -1,6 +1,6 @@
'GET') not_found();
diff --git a/src/public/legal/terms-of-service.php b/src/public/legal/terms-of-service.php
index 89d37a4..c5e6460 100644
--- a/src/public/legal/terms-of-service.php
+++ b/src/public/legal/terms-of-service.php
@@ -1,6 +1,6 @@
'GET') not_found();
@@ -24,7 +24,7 @@ Layout::pageHead('Terms of Service'); ?>
myPrayerJournal is a service that allows individuals to enter and amend their prayer requests. It
requires no registration by itself, but access is granted based on a successful login with an
external identity provider. See
- for details on how that
+ =Component::pageLink('/legal/privacy-policy', 'our privacy policy')?> for details on how that
information is accessed and stored.
@@ -53,7 +53,7 @@ Layout::pageHead('Terms of Service'); ?>
- You may also wish to review our to learn how
+ You may also wish to review our =Component::pageLink('/legal/privacy-policy', 'privacy policy')?> to learn how
we handle your data.
id, ['snoozedUntil']);
+$req->snoozedUntil = null;
// TODO: message
Layout::bareHead();
-UI::requestItem($req);
+Component::requestItem($req);
Layout::bareFoot();
diff --git a/src/public/request/edit.php b/src/public/request/edit.php
index 038c911..8d1238f 100644
--- a/src/public/request/edit.php
+++ b/src/public/request/edit.php
@@ -1,6 +1,8 @@
'GET') not_found();
@@ -97,8 +99,9 @@ Layout::pageHead("$action Prayer Request");?>
- =UI::icon('save');?> Save 'btn btn-secondary ms-2']); ?>
+ =Component::icon('save');?> Save
+ =Component::pageLink($cancelLink, Component::icon('arrow_back') . ' Cancel',
+ ['class' => 'btn btn-secondary ms-2'])?>
Answered =$answered->format('F j, Y')?>
- (=UI::formatDistance('now', $req->history[0]->asOf);?>) •history[0]->asOf);?>) •
Prayed =number_format($prayed)?> times • Open =number_format($daysOpen)?> days
diff --git a/src/public/request/note.php b/src/public/request/note.php
index d918bee..99cfcb1 100644
--- a/src/public/request/note.php
+++ b/src/public/request/note.php
@@ -1,7 +1,8 @@
recurrence->period <> RecurrencePeriod::Immediate) {
}
Patch::byId(Table::REQUEST, $req->id, $patch);
-UI::journal();
+Component::journal();
diff --git a/src/public/request/save.php b/src/public/request/save.php
index d1e0196..7d3bea1 100644
--- a/src/public/request/save.php
+++ b/src/public/request/save.php
@@ -1,7 +1,8 @@
'POST' && $_SERVER['REQUEST_METHOD'] <> 'PATCH') not_found();
diff --git a/src/public/request/show.php b/src/public/request/show.php
new file mode 100644
index 0000000..ef7b3e4
--- /dev/null
+++ b/src/public/request/show.php
@@ -0,0 +1,17 @@
+id, ['showAfter']);
+$req->showAfter = null;
+
+// TODO: message
+Layout::bareHead();
+Component::requestItem($req);
+Layout::bareFoot();
diff --git a/src/public/request/snooze.php b/src/public/request/snooze.php
index 62eb805..23e480d 100644
--- a/src/public/request/snooze.php
+++ b/src/public/request/snooze.php
@@ -1,7 +1,8 @@
id, ['snoozedUntil' => $until->format('c')]);
// TODO: message
hide_modal('snooze');
-UI::journal();
+Component::journal();
diff --git a/src/public/requests/active.php b/src/public/requests/active.php
index 3a13b29..3015bb6 100644
--- a/src/public/requests/active.php
+++ b/src/public/requests/active.php
@@ -1,6 +1,8 @@
'GET') not_found();
@@ -13,9 +15,9 @@ Layout::pageHead('Active Requests'); ?>
Active Requests hasItems()) {
- UI::requestList($reqs);
+ Component::requestList($reqs);
} else {
- UI::noResults('No Active Requests', '/journal', 'Return to your journal',
+ Component::noResults('No Active Requests', '/journal', 'Return to your journal',
'Your prayer journal has no active requests');
} ?>
'GET') not_found();
@@ -13,9 +15,9 @@ Layout::pageHead('Answered Requests'); ?>
Answered Requests hasItems()) {
- UI::requestList($reqs);
+ Component::requestList($reqs);
} else {
- UI::noResults('No Answered Requests', '/journal', 'Return to your journal', <<<'TEXT'
+ Component::noResults('No Answered Requests', '/journal', 'Return to your journal', <<<'TEXT'
Your prayer journal has no answered requests; once you have marked one as “Answered”, it will
appear here
TEXT);
diff --git a/src/public/requests/snoozed.php b/src/public/requests/snoozed.php
index 3652a32..ccc6cad 100644
--- a/src/public/requests/snoozed.php
+++ b/src/public/requests/snoozed.php
@@ -1,6 +1,8 @@
'GET') not_found();
@@ -13,9 +15,9 @@ Layout::pageHead('Snoozed Requests'); ?>
Snoozed Requests hasItems()) {
- UI::requestList($reqs);
+ Component::requestList($reqs);
} else {
- UI::noResults('No Snoozed Requests', '/journal', 'Return to your journal',
+ Component::noResults('No Snoozed Requests', '/journal', 'Return to your journal',
'Your prayer journal has no snoozed requests');
} ?>
'GET') not_found();
Auth::logOff();
diff --git a/src/public/user/log-on.php b/src/public/user/log-on.php
index e751fa7..34baaa1 100644
--- a/src/public/user/log-on.php
+++ b/src/public/user/log-on.php
@@ -2,7 +2,7 @@
use MyPrayerJournal\Auth;
-if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
require '../../start.php';
+if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
Auth::logOn();
diff --git a/src/start.php b/src/start.php
index 1a1e141..abe4103 100644
--- a/src/start.php
+++ b/src/start.php
@@ -3,7 +3,8 @@
use Auth0\SDK\Exception\ConfigurationException;
use BitBadger\PDODocument\{Configuration, Definition, DocumentException, Mode};
use Dotenv\Dotenv;
-use MyPrayerJournal\{Auth, Request, Table};
+use MyPrayerJournal\{Auth, Table};
+use MyPrayerJournal\Domain\Request;
require __DIR__ . '/vendor/autoload.php';