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
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,16 @@ class Layout
|
||||
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
|
||||
*
|
||||
@ -27,7 +37,7 @@ class Layout
|
||||
*/
|
||||
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>";
|
||||
} else {
|
||||
echo <<<HEAD
|
||||
@ -36,6 +46,7 @@ class Layout
|
||||
<head>
|
||||
<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=htmx-config content='{"historyCacheSize":0}'>
|
||||
<title>$title « myPrayerJournal</title>
|
||||
<link href=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css rel=stylesheet
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
@ -130,4 +141,30 @@ class Layout
|
||||
</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
|
||||
*/
|
||||
public function __construct(public string $asOf, public string $text) { }
|
||||
// AFU2SCY5X2BNVRXP6W47D369
|
||||
|
||||
/**
|
||||
* Retrieve notes for a given request
|
||||
*
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
use DateInterval;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
@ -15,10 +16,26 @@ class Recurrence implements JsonSerializable
|
||||
*/
|
||||
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
|
||||
{
|
||||
$values = ['period' => $this->period->value];
|
||||
if (!is_null($this->interval)) $values['interval'] = $this->interval;
|
||||
if (isset($this->interval)) $values['interval'] = $this->interval;
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,16 @@ class Request implements JsonSerializable
|
||||
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
|
||||
{
|
||||
$values = [
|
||||
@ -66,8 +76,8 @@ class Request implements JsonSerializable
|
||||
'history' => $this->history,
|
||||
'notes' => $this->notes
|
||||
];
|
||||
if (!is_null($this->snoozedUntil)) $values['snoozedUntil'] = $this->snoozedUntil;
|
||||
if (!is_null($this->showAfter)) $values['showAfter'] = $this->showAfter;
|
||||
if (isset($this->snoozedUntil)) $values['snoozedUntil'] = $this->snoozedUntil;
|
||||
if (isset($this->showAfter)) $values['showAfter'] = $this->showAfter;
|
||||
return $values;
|
||||
}
|
||||
|
||||
@ -107,4 +117,53 @@ class Request implements JsonSerializable
|
||||
ORDER BY coalesce(last_prayed, data->>'snoozedUntil', data->>'showAfter', data->>'$.history[0].asOf')
|
||||
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;
|
||||
|
||||
use BitBadger\PDODocument\DocumentException;
|
||||
use BitBadger\PDODocument\DocumentList;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
@ -20,6 +22,60 @@ class UI
|
||||
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
|
||||
*/
|
||||
@ -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);
|
||||
|
||||
use MyPrayerJournal\{Auth, Layout, Request, UI};
|
||||
use MyPrayerJournal\{Auth, UI};
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||
require '../../start.php';
|
||||
|
||||
Auth::requireUser(false);
|
||||
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
|
||||
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();
|
||||
UI::journal();
|
||||
|
@ -1,8 +1,11 @@
|
||||
<?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">
|
||||
<h2 class=mb-3>Documentation</h2>
|
||||
|
||||
@ -96,4 +99,4 @@ page_head('Documentation'); ?>
|
||||
and strengthen your prayer life.
|
||||
</ul>
|
||||
</article><?php
|
||||
page_foot();
|
||||
Layout::pageFoot();
|
||||
|
@ -1,8 +1,11 @@
|
||||
<?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">
|
||||
<p>
|
||||
<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
|
||||
site at the “Docs” link, also above.
|
||||
</article><?php
|
||||
page_foot();
|
||||
Layout::pageFoot();
|
||||
|
@ -1,15 +1,15 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
use MyPrayerJournal\UI;
|
||||
use MyPrayerJournal\{Auth, Layout, UI};
|
||||
|
||||
require '../start.php';
|
||||
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||
|
||||
Auth::requireUser();
|
||||
|
||||
$user = Auth::user();
|
||||
$name = $user['given_name'] ?? 'Your';
|
||||
page_head('Welcome'); ?>
|
||||
Layout::pageHead('Journal'); ?>
|
||||
<article class="container-fluid mt-3">
|
||||
<h2 class=pb-3><?=$name?><?=$name == 'Your' ? '' : '’s'?> Prayer Journal</h2>
|
||||
<p class="pb-3 text-center"><?php
|
||||
@ -46,5 +46,5 @@ page_head('Welcome'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</article><?php
|
||||
page_foot();
|
||||
Layout::pageFoot();
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
<?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">
|
||||
<h2 class=mb-2>Privacy Policy</h2>
|
||||
<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>
|
||||
</article><?php
|
||||
page_foot();
|
||||
Layout::pageFoot();
|
||||
|
@ -1,10 +1,11 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\UI;
|
||||
use MyPrayerJournal\{Layout, UI};
|
||||
|
||||
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">
|
||||
<h2 class=mb-2>Terms of Service</h2>
|
||||
<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
|
||||
we handle your data.
|
||||
</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;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||
require '../../start.php';
|
||||
|
||||
Auth::logOn();
|
||||
|
@ -14,9 +14,9 @@ const MPJ_VERSION = '4.0.0-alpha1';
|
||||
if (php_sapi_name() != 'cli') {
|
||||
session_start();
|
||||
|
||||
$auth0_session = Auth::client()->getCredentials();
|
||||
if (!is_null($auth0_session)) {
|
||||
$_SESSION['user_id'] = $auth0_session->user['sub'];
|
||||
$auth0_user = Auth::user();
|
||||
if (!is_null($auth0_user)) {
|
||||
$_SESSION['user_id'] = $auth0_user['sub'];
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,31 +24,14 @@ Configuration::$pdoDSN = 'sqlite:' . implode(DIRECTORY_SEPARATOR, [__DIR__, 'dat
|
||||
Configuration::$mode = Mode::SQLite;
|
||||
Definition::ensureTable(Table::REQUEST);
|
||||
|
||||
$_PATCH = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] ?? '' == 'PATCH') parse_str(file_get_contents('php://input'), $_PATCH);
|
||||
|
||||
/**
|
||||
* Is this an htmx request?
|
||||
*
|
||||
* @return bool True if this is an htmx request, false if not
|
||||
* Return a 404 and exit
|
||||
*/
|
||||
function is_htmx(): bool
|
||||
function not_found(): never
|
||||
{
|
||||
return key_exists('HTTP_HX_REQUEST', $_SERVER) && !key_exists('HTTP_HX_HISTORY_RESTORE_REQUEST', $_SERVER);
|
||||
}
|
||||
|
||||
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>';
|
||||
http_response_code(404);
|
||||
die('Not found');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user