First cut of Add / Edit / Active / Answered pages
- Centralized UI between UI and Layout classes
This commit is contained in:
parent
0b7fa77247
commit
b759c3494e
@ -19,7 +19,7 @@ class History implements JsonSerializable
|
|||||||
public function jsonSerialize(): mixed
|
public function jsonSerialize(): mixed
|
||||||
{
|
{
|
||||||
$values = ['asOf' => $this->asOf, 'action' => $this->action->value];
|
$values = ['asOf' => $this->asOf, 'action' => $this->action->value];
|
||||||
if (!is_null($this->text)) $values['text'] = $this->text;
|
if (isset($this->text)) $values['text'] = $this->text;
|
||||||
return $values;
|
return $values;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,16 @@ class Layout
|
|||||||
echo '</body></html>';
|
echo '</body></html>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this an htmx request?
|
||||||
|
*
|
||||||
|
* @return bool True if this is an htmx request, false if not
|
||||||
|
*/
|
||||||
|
private static function isHtmx(): bool
|
||||||
|
{
|
||||||
|
return key_exists('HTTP_HX_REQUEST', $_SERVER) && !key_exists('HTTP_HX_HISTORY_RESTORE_REQUEST', $_SERVER);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the `DOCTYPE` declaration, `html`, and `head` tags for the page
|
* Create the `DOCTYPE` declaration, `html`, and `head` tags for the page
|
||||||
*
|
*
|
||||||
@ -27,7 +37,7 @@ class Layout
|
|||||||
*/
|
*/
|
||||||
public static function htmlHead(string $title): void
|
public static function htmlHead(string $title): void
|
||||||
{
|
{
|
||||||
if (is_htmx()) {
|
if (self::isHtmx()) {
|
||||||
echo "<!DOCTYPE html><html lang=en><head lang=en><title>$title « myPrayerJournal</title></head>";
|
echo "<!DOCTYPE html><html lang=en><head lang=en><title>$title « myPrayerJournal</title></head>";
|
||||||
} else {
|
} else {
|
||||||
echo <<<HEAD
|
echo <<<HEAD
|
||||||
@ -36,6 +46,7 @@ class Layout
|
|||||||
<head>
|
<head>
|
||||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
<meta name=description content="Online prayer journal - free w/Google or Microsoft account">
|
<meta name=description content="Online prayer journal - free w/Google or Microsoft account">
|
||||||
|
<meta name=htmx-config content='{"historyCacheSize":0}'>
|
||||||
<title>$title « myPrayerJournal</title>
|
<title>$title « myPrayerJournal</title>
|
||||||
<link href=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css rel=stylesheet
|
<link href=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css rel=stylesheet
|
||||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||||
@ -130,4 +141,30 @@ class Layout
|
|||||||
</footer><?php
|
</footer><?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the heading for a full or partial page result
|
||||||
|
*
|
||||||
|
* @param string $title The title of the page
|
||||||
|
*/
|
||||||
|
public static function pageHead(string $title): void
|
||||||
|
{
|
||||||
|
self::htmlHead($title);
|
||||||
|
echo '<body>';
|
||||||
|
if (!self::isHtmx()) echo '<section id=top aria-label="Top navigation">';
|
||||||
|
self::navBar();
|
||||||
|
echo '<main role=main>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the end of the page for a full or partial page result
|
||||||
|
*/
|
||||||
|
public static function pageFoot(): void
|
||||||
|
{
|
||||||
|
echo '</main>';
|
||||||
|
if (!self::isHtmx()) {
|
||||||
|
echo '</section>';
|
||||||
|
self::htmlFoot();
|
||||||
|
}
|
||||||
|
echo '</body></html>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ class Note
|
|||||||
* @param string $text The text of the note
|
* @param string $text The text of the note
|
||||||
*/
|
*/
|
||||||
public function __construct(public string $asOf, public string $text) { }
|
public function __construct(public string $asOf, public string $text) { }
|
||||||
// AFU2SCY5X2BNVRXP6W47D369
|
|
||||||
/**
|
/**
|
||||||
* Retrieve notes for a given request
|
* Retrieve notes for a given request
|
||||||
*
|
*
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace MyPrayerJournal;
|
namespace MyPrayerJournal;
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,10 +16,26 @@ class Recurrence implements JsonSerializable
|
|||||||
*/
|
*/
|
||||||
public function __construct(public RecurrencePeriod $period, public ?int $interval = null) { }
|
public function __construct(public RecurrencePeriod $period, public ?int $interval = null) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the date/time interval for this recurrence
|
||||||
|
*
|
||||||
|
* @return DateInterval The interval matching the recurrence
|
||||||
|
*/
|
||||||
|
public function interval(): DateInterval
|
||||||
|
{
|
||||||
|
$period = match ($this->period) {
|
||||||
|
RecurrencePeriod::Immediate => 'T0S',
|
||||||
|
RecurrencePeriod::Hours => "T{$this->interval}H",
|
||||||
|
RecurrencePeriod::Days => "{$this->interval}D",
|
||||||
|
RecurrencePeriod::Weeks => ($this->interval * 7) . 'D'
|
||||||
|
};
|
||||||
|
return new DateInterval("P$period");
|
||||||
|
}
|
||||||
|
|
||||||
public function jsonSerialize(): mixed
|
public function jsonSerialize(): mixed
|
||||||
{
|
{
|
||||||
$values = ['period' => $this->period->value];
|
$values = ['period' => $this->period->value];
|
||||||
if (!is_null($this->interval)) $values['interval'] = $this->interval;
|
if (isset($this->interval)) $values['interval'] = $this->interval;
|
||||||
return $values;
|
return $values;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,16 @@ class Request implements JsonSerializable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has this request been answered?
|
||||||
|
*
|
||||||
|
* @return bool True if the request is answered, false if not
|
||||||
|
*/
|
||||||
|
public function isAnswered(): bool
|
||||||
|
{
|
||||||
|
return $this->history[0]->action == RequestAction::Answered;
|
||||||
|
}
|
||||||
|
|
||||||
public function jsonSerialize(): mixed
|
public function jsonSerialize(): mixed
|
||||||
{
|
{
|
||||||
$values = [
|
$values = [
|
||||||
@ -66,8 +76,8 @@ class Request implements JsonSerializable
|
|||||||
'history' => $this->history,
|
'history' => $this->history,
|
||||||
'notes' => $this->notes
|
'notes' => $this->notes
|
||||||
];
|
];
|
||||||
if (!is_null($this->snoozedUntil)) $values['snoozedUntil'] = $this->snoozedUntil;
|
if (isset($this->snoozedUntil)) $values['snoozedUntil'] = $this->snoozedUntil;
|
||||||
if (!is_null($this->showAfter)) $values['showAfter'] = $this->showAfter;
|
if (isset($this->showAfter)) $values['showAfter'] = $this->showAfter;
|
||||||
return $values;
|
return $values;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,4 +117,53 @@ class Request implements JsonSerializable
|
|||||||
ORDER BY coalesce(last_prayed, data->>'snoozedUntil', data->>'showAfter', data->>'$.history[0].asOf')
|
ORDER BY coalesce(last_prayed, data->>'snoozedUntil', data->>'showAfter', data->>'$.history[0].asOf')
|
||||||
SQL, [':userId' => $_SESSION['user_id']], new DocumentMapper(self::class));
|
SQL, [':userId' => $_SESSION['user_id']], new DocumentMapper(self::class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get either the user's active or answered requests
|
||||||
|
*
|
||||||
|
* @param bool $active True to retrieve active requests, false to retrieve answered requests
|
||||||
|
* @return DocumentList<Request> The requests matching the criteria
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
private static function forUser(bool $active = true): DocumentList
|
||||||
|
{
|
||||||
|
$table = Table::REQUEST;
|
||||||
|
$op = $active ? '<>' : '=';
|
||||||
|
$order = $active
|
||||||
|
? "coalesce(data->>'snoozedUntil', data->>'showAfter', last_prayed, data->>'$.history[0].asOf')"
|
||||||
|
: "data->>'$.history[0].asOf' DESC";
|
||||||
|
return Custom::list(<<<SQL
|
||||||
|
SELECT data, (
|
||||||
|
SELECT h.value->>'asOf' as_of
|
||||||
|
FROM $table i LEFT JOIN json_each(i.data, '$.history') h
|
||||||
|
WHERE r.data->>'id' = i.data->>'id' AND h.value->>'action' = 'Prayed'
|
||||||
|
LIMIT 1) last_prayed
|
||||||
|
FROM $table r
|
||||||
|
WHERE data->>'userId' = :userId
|
||||||
|
AND data->>'$.history[0].action' $op 'Answered'
|
||||||
|
ORDER BY $order
|
||||||
|
SQL, [':userId' => $_SESSION['user_id']], new DocumentMapper(self::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of active requests for a user
|
||||||
|
*
|
||||||
|
* @return DocumentList<Request> The user's active requests
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function active(): DocumentList
|
||||||
|
{
|
||||||
|
return self::forUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of answered requests for a user
|
||||||
|
*
|
||||||
|
* @return DocumentList<Request> The user's answered requests
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function answered(): DocumentList
|
||||||
|
{
|
||||||
|
return self::forUser(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace MyPrayerJournal;
|
namespace MyPrayerJournal;
|
||||||
|
|
||||||
|
use BitBadger\PDODocument\DocumentException;
|
||||||
|
use BitBadger\PDODocument\DocumentList;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
|
|
||||||
@ -20,6 +22,60 @@ class UI
|
|||||||
return "<span class=material-icons>$name</span>";
|
return "<span class=material-icons>$name</span>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()) { ?>
|
||||||
|
<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
|
||||||
|
$spacer = '<span> </span>';
|
||||||
|
foreach ($reqs->items() as /** @var Request $req */ $req) { ?>
|
||||||
|
<div class=col>
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header p-0 d-flex" role=toolbar><?php
|
||||||
|
self::pageLink("/request/edit?id=$req->id", self::icon('edit'),
|
||||||
|
['class' => 'btn btn-secondary', 'title' => 'Edit Request']); ?>
|
||||||
|
<?=$spacer?>
|
||||||
|
<button type=button class="btn btn-secondary" title="Add Notes" data-bs-toggle=modal
|
||||||
|
data-bs-target=#notesModal hx-get="/components/request/add-notes?id=<?=$req->id?>"
|
||||||
|
hx-target=#notesBody hx-swap=innerHTML><?=self::icon('comment');?></button>
|
||||||
|
<?=$spacer?>
|
||||||
|
<button type=button class="btn btn-secondary" title="Snooze Request" data-bs-toggle=modal
|
||||||
|
data-bs-target=#snoozeModal hx-get="/components/request/snooze?id=<?=$req->id?>"
|
||||||
|
hx-target=#snoozeBody hx-swap=innerHTML><?=self::icon('schedule');?></button>
|
||||||
|
<div class=flex-grow-1></div>
|
||||||
|
<button type=button class="btn btn-success w-25" hx-patch="/request/prayed?id=<?=$req->id?>"
|
||||||
|
title="Mark as Prayed"><?=self::icon('done');?></button>
|
||||||
|
</div>
|
||||||
|
<div class=card-body>
|
||||||
|
<p class=request-text><?=htmlentities($req->currentText());?>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-end text-muted px-1 py-0">
|
||||||
|
<em><?php
|
||||||
|
$lastPrayed = $req->lastPrayed();
|
||||||
|
echo 'last ' . (is_null($lastPrayed) ? 'activity': 'prayed') . ' ';
|
||||||
|
self::relativeDate($lastPrayed ?? $req->history[0]->asOf); ?>
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><?php
|
||||||
|
} ?>
|
||||||
|
</section><?php
|
||||||
|
} else {
|
||||||
|
UI::noResults('No Active Requests', '/request/edit?id=new', 'Add a Request', <<<'TEXT'
|
||||||
|
You have no requests to be shown; see the “Active” link above for snoozed or deferred
|
||||||
|
requests, and the “Answered” link for answered requests
|
||||||
|
TEXT);
|
||||||
|
}
|
||||||
|
Layout::bareFoot();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a card when there are no results found
|
* Create a card when there are no results found
|
||||||
*/
|
*/
|
||||||
@ -76,4 +132,42 @@ class UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function requestList(DocumentList $reqs): void
|
||||||
|
{
|
||||||
|
$btnClass = "btn btn-light mx-2";
|
||||||
|
/// Create a request within the list
|
||||||
|
/* let reqListItem now tz req =
|
||||||
|
let isFuture instant = defaultArg (instant |> Option.map (fun it -> it > now)) false
|
||||||
|
let reqId = RequestId.toString req.RequestId
|
||||||
|
let isSnoozed = isFuture req.SnoozedUntil
|
||||||
|
let isPending = (not isSnoozed) && isFuture req.ShowAfter
|
||||||
|
let restoreBtn (link : string) title =
|
||||||
|
button [ btnClass; _hxPatch $"/request/{reqId}/{link}"; _title title ] [ icon "restore" ] */ ?>
|
||||||
|
<div class=list-group><?php
|
||||||
|
foreach ($reqs->items() as /** @var Request $req */ $req) { ?>
|
||||||
|
<div class="list-group-item px-0 d-flex flex-row align-items-start" hx-target=this
|
||||||
|
hx-swap=outerHTML><?php
|
||||||
|
self::pageLink("/request/full?id=$req->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 isSnoozed then restoreBtn "cancel-snooze" "Cancel Snooze"
|
||||||
|
// elif isPending then restoreBtn "show" "Show Now"
|
||||||
|
|
||||||
|
echo '<p class="request-text mb-0">' . $req->currentText();
|
||||||
|
// if isSnoozed || isPending || isAnswered then
|
||||||
|
// br []
|
||||||
|
// small [ _class "text-muted" ] [
|
||||||
|
// if isSnoozed then [ str "Snooze expires "; relativeDate req.SnoozedUntil.Value now tz ]
|
||||||
|
// elif isPending then [ str "Request appears next "; relativeDate req.ShowAfter.Value now tz ]
|
||||||
|
// else (* isAnswered *) [ str "Answered "; relativeDate req.AsOf now tz ]
|
||||||
|
// |> em []
|
||||||
|
// ]
|
||||||
|
?>
|
||||||
|
</div><?php
|
||||||
|
} ?>
|
||||||
|
</div><?php
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,54 +1,10 @@
|
|||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
use MyPrayerJournal\{Auth, Layout, Request, UI};
|
use MyPrayerJournal\{Auth, UI};
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
require '../../start.php';
|
require '../../start.php';
|
||||||
|
|
||||||
Auth::requireUser(false);
|
Auth::requireUser(false);
|
||||||
Layout::bareHead();
|
|
||||||
|
|
||||||
$reqs = Request::forJournal();
|
UI::journal();
|
||||||
if ($reqs->hasItems()) { ?>
|
|
||||||
<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
|
|
||||||
$spacer = '<span> </span>';
|
|
||||||
foreach ($reqs->items() as /** @var Request $req */ $req) { ?>
|
|
||||||
<div class=col>
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-header p-0 d-flex" role=toolbar><?php
|
|
||||||
UI::pageLink("/request/edit?id=$req->id", UI::icon('edit'),
|
|
||||||
['class' => 'btn btn-secondary', 'title' => 'Edit Request']); ?>
|
|
||||||
<?=$spacer?>
|
|
||||||
<button type=button class="btn btn-secondary" title="Add Notes" data-bs-toggle=modal
|
|
||||||
data-bs-target=#notesModal hx-get="/components/request/add-notes?id=<?=$req->id?>"
|
|
||||||
hx-target=#notesBody hx-swap=innerHTML><?=UI::icon('comment');?></button>
|
|
||||||
<?=$spacer?>
|
|
||||||
<button type=button class="btn btn-secondary" title="Snooze Request" data-bs-toggle=modal
|
|
||||||
data-bs-target=#snoozeModal hx-get="/components/request/snooze?id=<?=$req->id?>"
|
|
||||||
hx-target=#snoozeBody hx-swap=innerHTML><?=UI::icon('schedule');?></button>
|
|
||||||
<div class=flex-grow-1></div>
|
|
||||||
<button type=button class="btn btn-success w-25" hx-patch="/request/prayed?id=<?=$req->id?>"
|
|
||||||
title="Mark as Prayed"><?=UI::icon('done');?></button>
|
|
||||||
</div>
|
|
||||||
<div class=card-body>
|
|
||||||
<p class=request-text><?=htmlentities($req->currentText());?>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer text-end text-muted px-1 py-0">
|
|
||||||
<em><?php
|
|
||||||
$lastPrayed = $req->lastPrayed();
|
|
||||||
echo 'last ' . (is_null($lastPrayed) ? 'activity': 'prayed') . ' ';
|
|
||||||
UI::relativeDate($lastPrayed ?? $req->history[0]->asOf); ?>
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div><?php
|
|
||||||
} ?>
|
|
||||||
</section><?php
|
|
||||||
} else {
|
|
||||||
UI::noResults('No Active Requests', '/request/edit?id=new', 'Add a Request', <<<'TEXT'
|
|
||||||
You have no requests to be shown; see the “Active” link above for snoozed or deferred requests, and
|
|
||||||
the “Answered” link for answered requests
|
|
||||||
TEXT);
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout::bareFoot();
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
require '../start.php';
|
use MyPrayerJournal\Layout;
|
||||||
|
|
||||||
page_head('Documentation'); ?>
|
require '../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
|
|
||||||
|
Layout::pageHead('Documentation'); ?>
|
||||||
<article class="container mt-3">
|
<article class="container mt-3">
|
||||||
<h2 class=mb-3>Documentation</h2>
|
<h2 class=mb-3>Documentation</h2>
|
||||||
|
|
||||||
@ -96,4 +99,4 @@ page_head('Documentation'); ?>
|
|||||||
and strengthen your prayer life.
|
and strengthen your prayer life.
|
||||||
</ul>
|
</ul>
|
||||||
</article><?php
|
</article><?php
|
||||||
page_foot();
|
Layout::pageFoot();
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
require '../start.php';
|
use MyPrayerJournal\Layout;
|
||||||
|
|
||||||
page_head('Welcome'); ?>
|
require '../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
|
|
||||||
|
Layout::pageHead('Welcome'); ?>
|
||||||
<article class="container mt-3">
|
<article class="container mt-3">
|
||||||
<p>
|
<p>
|
||||||
<p>myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them,
|
<p>myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them,
|
||||||
@ -12,4 +15,4 @@ page_head('Welcome'); ?>
|
|||||||
On” link above, and log on with either a Microsoft or Google account. You can also learn more about the
|
On” link above, and log on with either a Microsoft or Google account. You can also learn more about the
|
||||||
site at the “Docs” link, also above.
|
site at the “Docs” link, also above.
|
||||||
</article><?php
|
</article><?php
|
||||||
page_foot();
|
Layout::pageFoot();
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
use MyPrayerJournal\Auth;
|
use MyPrayerJournal\{Auth, Layout, UI};
|
||||||
use MyPrayerJournal\UI;
|
|
||||||
|
|
||||||
require '../start.php';
|
require '../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
|
|
||||||
Auth::requireUser();
|
Auth::requireUser();
|
||||||
|
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$name = $user['given_name'] ?? 'Your';
|
$name = $user['given_name'] ?? 'Your';
|
||||||
page_head('Welcome'); ?>
|
Layout::pageHead('Journal'); ?>
|
||||||
<article class="container-fluid mt-3">
|
<article class="container-fluid mt-3">
|
||||||
<h2 class=pb-3><?=$name?><?=$name == 'Your' ? '' : '’s'?> Prayer Journal</h2>
|
<h2 class=pb-3><?=$name?><?=$name == 'Your' ? '' : '’s'?> Prayer Journal</h2>
|
||||||
<p class="pb-3 text-center"><?php
|
<p class="pb-3 text-center"><?php
|
||||||
@ -46,5 +46,5 @@ page_head('Welcome'); ?>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article><?php
|
</article><?php
|
||||||
page_foot();
|
Layout::pageFoot();
|
||||||
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
require '../../start.php';
|
use MyPrayerJournal\Layout;
|
||||||
|
|
||||||
page_head('Privacy Policy'); ?>
|
require '../../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
|
|
||||||
|
Layout::pageHead('Privacy Policy'); ?>
|
||||||
<article class="container mt-3">
|
<article class="container mt-3">
|
||||||
<h2 class=mb-2>Privacy Policy</h2>
|
<h2 class=mb-2>Privacy Policy</h2>
|
||||||
<h6 class="text-muted pb-3">as of May 21<sup>st</sup>, 2018</h6>
|
<h6 class="text-muted pb-3">as of May 21<sup>st</sup>, 2018</h6>
|
||||||
@ -66,4 +69,4 @@ page_head('Privacy Policy'); ?>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article><?php
|
</article><?php
|
||||||
page_foot();
|
Layout::pageFoot();
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
use MyPrayerJournal\UI;
|
use MyPrayerJournal\{Layout, UI};
|
||||||
|
|
||||||
require '../../start.php';
|
require '../../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
|
|
||||||
page_head('Terms of Service'); ?>
|
Layout::pageHead('Terms of Service'); ?>
|
||||||
<article class="container mt-3">
|
<article class="container mt-3">
|
||||||
<h2 class=mb-2>Terms of Service</h2>
|
<h2 class=mb-2>Terms of Service</h2>
|
||||||
<h6 class="text-muted pb-3">as of May 21<sup>st</sup>, 2018</h6>
|
<h6 class="text-muted pb-3">as of May 21<sup>st</sup>, 2018</h6>
|
||||||
@ -55,4 +56,4 @@ page_head('Terms of Service'); ?>
|
|||||||
You may also wish to review our <?php UI::pageLink('/legal/privacy-policy', 'privacy policy'); ?> to learn how
|
You may also wish to review our <?php UI::pageLink('/legal/privacy-policy', 'privacy policy'); ?> to learn how
|
||||||
we handle your data.
|
we handle your data.
|
||||||
</article><?php
|
</article><?php
|
||||||
page_foot();
|
Layout::pageFoot();
|
||||||
|
105
src/public/request/edit.php
Normal file
105
src/public/request/edit.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
use MyPrayerJournal\{Auth, Layout, RecurrencePeriod, Request, RequestAction, UI};
|
||||||
|
|
||||||
|
require '../../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
|
|
||||||
|
Auth::requireUser();
|
||||||
|
|
||||||
|
$isNew = $_GET['id'] == 'new';
|
||||||
|
|
||||||
|
$req = match ($isNew) {
|
||||||
|
true => new Request('new'),
|
||||||
|
false => Request::byId($_GET['id'])
|
||||||
|
};
|
||||||
|
if (!$req) not_found();
|
||||||
|
|
||||||
|
$cancelLink = match (true) {
|
||||||
|
str_ends_with($_SERVER['HTTP_REFERER'] ?? '', 'active.php') => '/requests/active',
|
||||||
|
str_ends_with($_SERVER['HTTP_REFERER'] ?? '', 'snoozed.php') => '/requests/snoozed',
|
||||||
|
default => '/journal'
|
||||||
|
};
|
||||||
|
$action = $_GET['id'] == 'new' ? 'Add' : 'Edit';
|
||||||
|
|
||||||
|
Layout::pageHead("$action Prayer Request");?>
|
||||||
|
<article class=container>
|
||||||
|
<h2 class=pb-3><?=$action?> Prayer Request</h2>
|
||||||
|
<form <?=$isNew ? 'hx-post' : 'hx-patch'?>=/request/save hx-target=#top hx-push-url=true>
|
||||||
|
<input type=hidden name=requestId value=<?=$req->id?>>
|
||||||
|
<input type=hidden name=returnTo value=<?=$cancelLink?>>
|
||||||
|
<div class="form-floating pb-3">
|
||||||
|
<textarea id=requestText name=requestText class=form-control style="min-height: 8rem;"
|
||||||
|
placeholder="Enter the text of the request" autofocus required><?=$req->currentText()?></textarea>
|
||||||
|
<label for=requestText>Prayer Request</label>
|
||||||
|
</div><br><?php
|
||||||
|
if (!$isNew) { ?>
|
||||||
|
<div class=pb-3>
|
||||||
|
<label>Also Mark As</label><br>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input type=radio class=form-check-input id=sU name=status value=<?=RequestAction::Updated->value?>
|
||||||
|
checked>
|
||||||
|
<label for=sU>Updated</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input type=radio class=form-check-input id=sP name=status value=<?=RequestAction::Prayed->value?>>
|
||||||
|
<label for=sP>Prayed</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input type=radio class=form-check-input id=sA name=status
|
||||||
|
value=<?=RequestAction::Answered->value?>>
|
||||||
|
<label for=sA>Answered</label>
|
||||||
|
</div>
|
||||||
|
</div><?php
|
||||||
|
} ?>
|
||||||
|
<div class=row">
|
||||||
|
<div class="col-12 offset-md-2 col-md-8 offset-lg-3 col-lg-6">
|
||||||
|
<p><strong>Recurrence </strong> <em class=text-muted>After prayer, request reappears…</em>
|
||||||
|
<div class="d-flex flex-row flex-wrap justify-content-center align-items-center">
|
||||||
|
<div class="form-check mx-2">
|
||||||
|
<input type=radio class=form-check-input id=rI name=recurType
|
||||||
|
value=<?=RecurrencePeriod::Immediate->value?>
|
||||||
|
onclick="mpj.edit.toggleRecurrence(event)"<?php
|
||||||
|
if ($req->recurrence->period == RecurrencePeriod::Immediate) echo ' checked'; ?>>
|
||||||
|
<label for=rI>Immediately</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mx-2">
|
||||||
|
<input type=radio class=form-check-input id=rO name=recurType value=Other
|
||||||
|
onclick="mpj.edit.toggleRecurrence(event)"<?php
|
||||||
|
if ($req->recurrence->period <> RecurrencePeriod::Immediate) echo ' checked'; ?>>
|
||||||
|
<label for=rO>Every…</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating mx-2">
|
||||||
|
<input type=number class=form-control id=recurCount name=recurCount placeholder=0 required
|
||||||
|
value=<?=$req->recurrence->interval ?? 0?> style="width:6rem;"<?php
|
||||||
|
if ($req->recurrence->period == RecurrencePeriod::Immediate) echo ' disabled'; ?>>
|
||||||
|
<label for=recurCount>Count</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating mx-2">
|
||||||
|
<select class=form-control id=recurInterval name=recurInterval style="width:6rem;" required<?php
|
||||||
|
if ($req->recurrence->period == RecurrencePeriod::Immediate) echo ' disabled'; ?>>
|
||||||
|
<option value=<?=RecurrencePeriod::Hours->value?><?php
|
||||||
|
if ($req->recurrence->period == RecurrencePeriod::Hours) echo ' selected'; ?>>
|
||||||
|
hours
|
||||||
|
</option>
|
||||||
|
<option value=<?=RecurrencePeriod::Days->value?><?php
|
||||||
|
if ($req->recurrence->period == RecurrencePeriod::Days) echo ' selected'; ?>>
|
||||||
|
days
|
||||||
|
</option>
|
||||||
|
<option value=<?=RecurrencePeriod::Weeks->value?><?php
|
||||||
|
if ($req->recurrence->period == RecurrencePeriod::Weeks) echo ' selected'; ?>>
|
||||||
|
weeks
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label for=recurInterval>Interval</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end pt-3">
|
||||||
|
<button class="btn btn-primary me-2" type=submit><?=UI::icon('save');?> Save</button><?php
|
||||||
|
UI::pageLink($cancelLink, UI::icon('arrow_back') . ' Cancel', ['class' => 'btn btn-secondary ms-2']); ?>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</article><?php
|
||||||
|
Layout::pageFoot();
|
49
src/public/request/full.php
Normal file
49
src/public/request/full.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
use MyPrayerJournal\{Auth, History, Layout, Note, Request, RequestAction, UI};
|
||||||
|
|
||||||
|
require '../../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
|
|
||||||
|
Auth::requireUser();
|
||||||
|
|
||||||
|
$req = Request::byId($_GET['id']);
|
||||||
|
if (!$req) not_found();
|
||||||
|
|
||||||
|
$answered = $req->isAnswered() ? new DateTimeImmutable($req->history[0]->asOf) : null;
|
||||||
|
$prayed = sizeof(array_filter($req->history, fn(History $hist) => $hist->action == RequestAction::Prayed));
|
||||||
|
$daysOpen =
|
||||||
|
(($answered ?? new DateTimeImmutable('now'))->getTimestamp()
|
||||||
|
- (new DateTimeImmutable(end($req->history)->asOf))->getTimestamp()) / 86400;
|
||||||
|
|
||||||
|
$logs = array_merge(
|
||||||
|
array_map(fn(Note $note) => [new DateTimeImmutable($note->asOf), 'Notes', $note->text], $req->notes),
|
||||||
|
array_map(fn(History $hist) => [new DateTimeImmutable($hist->asOf), $hist->action->value, $hist->text ?? ''],
|
||||||
|
$req->history));
|
||||||
|
usort($logs, fn($a, $b) => $a[0] > $b[0] ? -1 : 1);
|
||||||
|
if ($req->isAnswered()) array_shift($logs);
|
||||||
|
|
||||||
|
Layout::pageHead('Full Request');?>
|
||||||
|
<article class="container mt-3">
|
||||||
|
<div class=card>
|
||||||
|
<h5 class=card-header>Full Prayer Request</h5>
|
||||||
|
<div class=card-body>
|
||||||
|
<h6 class="card-subtitle text-muted mb-2"><?php
|
||||||
|
if (!is_null($answered)) { ?>
|
||||||
|
Answered <?=$answered->format('F j, Y')?> (<?php UI::relativeDate($req->history[0]->asOf); ?>)
|
||||||
|
•<?php
|
||||||
|
} ?>
|
||||||
|
Prayed <?=number_format($prayed)?> times • Open <?=number_format($daysOpen)?> days
|
||||||
|
</h6>
|
||||||
|
<p class=card-text><?=htmlentities($req->currentText())?>
|
||||||
|
</div>
|
||||||
|
<ul class="list-group list-group-flush"><?php
|
||||||
|
foreach ($logs as $log) { ?>
|
||||||
|
<li class=list-group-item>
|
||||||
|
<p class=m-0><?=$log[1]?> <small><em><?=$log[0]->format('F j, Y')?></em></small><?php
|
||||||
|
if ($log[2] <> '') echo '<p class="mt-2 mb-0">' . htmlentities($log[2]);
|
||||||
|
} ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</article><?php
|
||||||
|
Layout::pageFoot();
|
24
src/public/request/prayed.php
Normal file
24
src/public/request/prayed.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
use MyPrayerJournal\{Auth, History, RecurrencePeriod, Request, RequestAction, Table, UI};
|
||||||
|
use BitBadger\PDODocument\Patch;
|
||||||
|
|
||||||
|
require '../../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'PATCH') not_found();
|
||||||
|
|
||||||
|
Auth::requireUser(false);
|
||||||
|
|
||||||
|
$req = Request::byId($_GET['id']);
|
||||||
|
if (!$req) not_found();
|
||||||
|
|
||||||
|
$now = new DateTimeImmutable('now');
|
||||||
|
|
||||||
|
array_unshift($req->history, new History($now->format('c'), RequestAction::Prayed));
|
||||||
|
$patch = ['history' => $req->history];
|
||||||
|
|
||||||
|
if ($req->recurrence->period <> RecurrencePeriod::Immediate) {
|
||||||
|
$patch['showAfter'] = $now->add($req->recurrence->interval())->format('c');
|
||||||
|
}
|
||||||
|
Patch::byId(Table::REQUEST, $req->id, $patch);
|
||||||
|
|
||||||
|
UI::journal();
|
51
src/public/request/save.php
Normal file
51
src/public/request/save.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
use MyPrayerJournal\{Auth, History, Recurrence, RecurrencePeriod, Request, RequestAction, Table};
|
||||||
|
use BitBadger\PDODocument\{Document, Patch, RemoveFields};
|
||||||
|
|
||||||
|
require '../../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'POST' && $_SERVER['REQUEST_METHOD'] <> 'PATCH') not_found();
|
||||||
|
|
||||||
|
Auth::requireUser(false);
|
||||||
|
|
||||||
|
$now = new DateTimeImmutable('now');
|
||||||
|
$recurrence = new Recurrence(RecurrencePeriod::from($_POST['recurType'] ?? $_PATCH['recurType']));
|
||||||
|
if ($recurrence->period <> RecurrencePeriod::Immediate) {
|
||||||
|
$recurrence->interval = (int)($_POST['recurCount'] ?? $_PATCH['recurCount']);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($_SERVER['REQUEST_METHOD']) {
|
||||||
|
case 'POST':
|
||||||
|
Document::insert(Table::REQUEST, new Request(
|
||||||
|
enteredOn: $now->format('c'),
|
||||||
|
userId: $_SESSION['user_id'],
|
||||||
|
recurrence: $recurrence,
|
||||||
|
history: [new History($now->format('c'), RequestAction::Created, $_POST['requestText'])]));
|
||||||
|
//Messages.pushSuccess ctx "Added prayer request" "/journal"
|
||||||
|
header('Location: /journal');
|
||||||
|
http_response_code(303);
|
||||||
|
exit;
|
||||||
|
|
||||||
|
case 'PATCH':
|
||||||
|
$req = Request::byId($_PATCH['requestId']);
|
||||||
|
if (!$req) not_found();
|
||||||
|
$patch = [];
|
||||||
|
// update recurrence if changed
|
||||||
|
if ($recurrence != $req->recurrence) {
|
||||||
|
$patch['recurrence'] = $recurrence;
|
||||||
|
if ($recurrence->period == RecurrencePeriod::Immediate) {
|
||||||
|
RemoveFields::byId(Table::REQUEST, $req->id, ['showAfter']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// append history
|
||||||
|
$upd8Text = trim($_PATCH['requestText']);
|
||||||
|
$text = $upd8Text == '' || $upd8Text == $req->currentText() ? null : $upd8Text;
|
||||||
|
array_unshift($req->history, new History($now->format('c'), RequestAction::from($_PATCH['status']), $text));
|
||||||
|
$patch['history'] = $req->history;
|
||||||
|
Patch::byId(Table::REQUEST, $req->id, $patch);
|
||||||
|
//Messages.pushSuccess ctx "Prayer request updated successfully" nextUrl
|
||||||
|
// TODO: make redirect that filters out non-local URLs
|
||||||
|
header('Location: ' . $_PATCH['returnTo']);
|
||||||
|
http_response_code(303);
|
||||||
|
exit;
|
||||||
|
}
|
22
src/public/requests/active.php
Normal file
22
src/public/requests/active.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
use MyPrayerJournal\{Auth, Layout, Request, UI};
|
||||||
|
|
||||||
|
require '../../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
|
|
||||||
|
Auth::requireUser();
|
||||||
|
|
||||||
|
$reqs = Request::active();
|
||||||
|
|
||||||
|
Layout::pageHead('Active Requests'); ?>
|
||||||
|
<article class="container mt-3">
|
||||||
|
<h2 class=pb-3>Active Requests</h2><?php
|
||||||
|
if ($reqs->hasItems()) {
|
||||||
|
UI::requestList($reqs);
|
||||||
|
} else {
|
||||||
|
UI::noResults('No Active Requests', '/journal', 'Return to your journal',
|
||||||
|
'Your prayer journal has no active requests');
|
||||||
|
} ?>
|
||||||
|
</article><?php
|
||||||
|
Layout::pageFoot();
|
24
src/public/requests/answered.php
Normal file
24
src/public/requests/answered.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
use MyPrayerJournal\{Auth, Layout, Request, UI};
|
||||||
|
|
||||||
|
require '../../start.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
|
|
||||||
|
Auth::requireUser();
|
||||||
|
|
||||||
|
$reqs = Request::answered();
|
||||||
|
|
||||||
|
Layout::pageHead('Answered Requests'); ?>
|
||||||
|
<article class="container mt-3">
|
||||||
|
<h2 class=pb-3>Answered Requests</h2><?php
|
||||||
|
if ($reqs->hasItems()) {
|
||||||
|
UI::requestList($reqs);
|
||||||
|
} else {
|
||||||
|
UI::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);
|
||||||
|
} ?>
|
||||||
|
</article><?php
|
||||||
|
Layout::pageFoot();
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use MyPrayerJournal\Auth;
|
use MyPrayerJournal\Auth;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||||
require '../../start.php';
|
require '../../start.php';
|
||||||
|
|
||||||
Auth::logOn();
|
Auth::logOn();
|
||||||
|
@ -14,9 +14,9 @@ const MPJ_VERSION = '4.0.0-alpha1';
|
|||||||
if (php_sapi_name() != 'cli') {
|
if (php_sapi_name() != 'cli') {
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
$auth0_session = Auth::client()->getCredentials();
|
$auth0_user = Auth::user();
|
||||||
if (!is_null($auth0_session)) {
|
if (!is_null($auth0_user)) {
|
||||||
$_SESSION['user_id'] = $auth0_session->user['sub'];
|
$_SESSION['user_id'] = $auth0_user['sub'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,31 +24,14 @@ Configuration::$pdoDSN = 'sqlite:' . implode(DIRECTORY_SEPARATOR, [__DIR__, 'dat
|
|||||||
Configuration::$mode = Mode::SQLite;
|
Configuration::$mode = Mode::SQLite;
|
||||||
Definition::ensureTable(Table::REQUEST);
|
Definition::ensureTable(Table::REQUEST);
|
||||||
|
|
||||||
|
$_PATCH = [];
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] ?? '' == 'PATCH') parse_str(file_get_contents('php://input'), $_PATCH);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this an htmx request?
|
* Return a 404 and exit
|
||||||
*
|
|
||||||
* @return bool True if this is an htmx request, false if not
|
|
||||||
*/
|
*/
|
||||||
function is_htmx(): bool
|
function not_found(): never
|
||||||
{
|
{
|
||||||
return key_exists('HTTP_HX_REQUEST', $_SERVER) && !key_exists('HTTP_HX_HISTORY_RESTORE_REQUEST', $_SERVER);
|
http_response_code(404);
|
||||||
}
|
die('Not found');
|
||||||
|
|
||||||
function page_head(string $title): void
|
|
||||||
{
|
|
||||||
Layout::htmlHead($title);
|
|
||||||
echo '<body>';
|
|
||||||
if (!is_htmx()) echo '<section id=top aria-label="Top navigation">';
|
|
||||||
Layout::navBar();
|
|
||||||
echo '<main role=main>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function page_foot(): void
|
|
||||||
{
|
|
||||||
echo '</main>';
|
|
||||||
if (!is_htmx()) {
|
|
||||||
echo '</section>';
|
|
||||||
Layout::htmlFoot();
|
|
||||||
}
|
|
||||||
echo '</body></html>';
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user