First cut of Add / Edit / Active / Answered pages

- Centralized UI between UI and Layout classes
This commit is contained in:
Daniel J. Summers 2024-06-22 16:58:33 -04:00
parent 0b7fa77247
commit b759c3494e
20 changed files with 528 additions and 96 deletions

View File

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

View File

@ -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 &#xab; myPrayerJournal</title></head>"; echo "<!DOCTYPE html><html lang=en><head lang=en><title>$title &#xab; 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 &#xab; myPrayerJournal</title> <title>$title &#xab; 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>';
}
} }

View File

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

View File

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

View File

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

View File

@ -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>&nbsp;</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 &ldquo;Active&rdquo; link above for snoozed or deferred
requests, and the &ldquo;Answered&rdquo; 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
}
} }

View File

@ -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>&nbsp;</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 &ldquo;Active&rdquo; link above for snoozed or deferred requests, and
the &ldquo;Answered&rdquo; link for answered requests
TEXT);
}
Layout::bareFoot();

View File

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

View File

@ -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>&nbsp; <p>&nbsp;
<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&rdquo; link above, and log on with either a Microsoft or Google account. You can also learn more about the On&rdquo; link above, and log on with either a Microsoft or Google account. You can also learn more about the
site at the &ldquo;Docs&rdquo; link, also above. site at the &ldquo;Docs&rdquo; link, also above.
</article><?php </article><?php
page_foot(); Layout::pageFoot();

View File

@ -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' ? '' : '&rsquo;s'?> Prayer Journal</h2> <h2 class=pb-3><?=$name?><?=$name == 'Your' ? '' : '&rsquo;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();

View File

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

View File

@ -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
View 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 &nbsp;</strong> <em class=text-muted>After prayer, request reappears&hellip;</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&hellip;</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();

View 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); ?>)
&bull;<?php
} ?>
Prayed <?=number_format($prayed)?> times &bull; 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]?>&nbsp; <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();

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

View 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;
}

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

View 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 &ldquo;Answered&rdquo;, it will
appear here
TEXT);
} ?>
</article><?php
Layout::pageFoot();

View File

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

View File

@ -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>';
} }