Compare commits

..

5 Commits

46 changed files with 911 additions and 514 deletions

3
.gitignore vendored
View File

@ -5,3 +5,6 @@
# PHP ignore files # PHP ignore files
src/vendor src/vendor
src/.env src/.env
# databases
src/data/*.db

View File

@ -1,15 +1,15 @@
{ {
"name": "bit-badger/my-prayer-journal", "name": "bit-badger/my-prayer-journal",
"minimum-stability": "beta",
"require": { "require": {
"php": ">=8.2", "php": ">=8.4",
"ext-pdo": "*", "ext-pdo": "*",
"ext-sqlite3": "*", "ext-sqlite3": "*",
"bit-badger/pdo-document": "^1", "auth0/auth0-php": "^8.11",
"bit-badger/pdo-document": "^2",
"guzzlehttp/guzzle": "^7.8", "guzzlehttp/guzzle": "^7.8",
"guzzlehttp/psr7": "^2.6", "guzzlehttp/psr7": "^2.6",
"http-interop/http-factory-guzzle": "^1.2", "http-interop/http-factory-guzzle": "^1.2",
"auth0/auth0-php": "^8.11", "square/pjson": "^0.5",
"vlucas/phpdotenv": "^5.6" "vlucas/phpdotenv": "^5.6"
}, },
"autoload": { "autoload": {

558
src/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,14 +10,13 @@ require 'start.php';
echo 'Retrieving v3 requests...' . PHP_EOL; echo 'Retrieving v3 requests...' . PHP_EOL;
Configuration::resetPDO(); Configuration::resetPDO();
Configuration::$pdoDSN = 'pgsql:host=localhost;user=mpj;password=devpassword;dbname=mpj'; Configuration::useDSN('pgsql:host=localhost;user=mpj;password=devpassword;dbname=mpj');
$reqs = Custom::array('SELECT data FROM mpj.request', [], new ArrayMapper()); $reqs = Custom::array('SELECT data FROM mpj.request', [], new ArrayMapper());
echo 'Found ' . sizeof($reqs) . ' requests; migrating to v4...' . PHP_EOL; echo 'Found ' . sizeof($reqs) . ' requests; migrating to v4...' . PHP_EOL;
Configuration::resetPDO(); Configuration::resetPDO();
Configuration::$mode = Mode::SQLite; Configuration::useDSN('sqlite:./data/mpj.db');
Configuration::$pdoDSN = 'sqlite:./data/mpj.db';
Configuration::$autoId = AutoId::RandomString; Configuration::$autoId = AutoId::RandomString;
Configuration::$idStringLength = 12; Configuration::$idStringLength = 12;
@ -26,7 +25,7 @@ Definition::ensureTable(Table::REQUEST);
/** Convert dates to the same format */ /** Convert dates to the same format */
function convertDate(string $date): string function convertDate(string $date): string
{ {
return (new DateTimeImmutable($date))->format('c'); return new DateTimeImmutable($date)->format('c');
} }
foreach ($reqs as $reqJson) { foreach ($reqs as $reqJson) {

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal; namespace MyPrayerJournal;

View File

@ -1,25 +1,53 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal\Domain; namespace MyPrayerJournal\Domain;
use JsonSerializable; use BitBadger\InspiredByFSharp\Option;
use Square\Pjson\{Json, JsonSerialize};
/** /**
* A record of an action taken on a request * A record of an action taken on a request
*/ */
class History implements JsonSerializable class History
{ {
use JsonSerialize;
/** @var string The date/time this entry was made */
#[Json]
public string $asOf = '';
/** @var RequestAction The action taken for this history entry */
#[Json]
public RequestAction $action;
/** @var string|null The text for this history entry (optional) */
#[Json('text', omit_empty: true)]
private ?string $dbText {
get => $this->text->unwrap();
set { $this->text = Option::of($value); }
}
/** @var Option<string> The text for this history entry */
public Option $text {
get => $this->text ?? Option::None();
set { $this->text = $value; }
}
/** /**
* @param string $asOf The date/time this entry was made * @param string $asOf The date/time this entry was made
* @param RequestAction $action The action taken for this history entry * @param RequestAction $action The action taken for this history entry
* @param string|null $text The text for this history entry (optional) * @param string|null $text The text for this history entry (optional)
*/ */
public function __construct(public string $asOf, public RequestAction $action, public ?string $text = null) { } public function __construct(string $asOf, RequestAction $action, ?string $text = null)
public function jsonSerialize(): mixed
{ {
$values = ['asOf' => $this->asOf, 'action' => $this->action->value]; $this->asOf = $asOf;
if (isset($this->text)) $values['text'] = $this->text; $this->action = $action;
return $values; $this->dbText = $text;
} }
} }

View File

@ -1,19 +1,29 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal\Domain; namespace MyPrayerJournal\Domain;
use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\DocumentException; use BitBadger\PDODocument\DocumentException;
use Square\Pjson\{Json, JsonSerialize};
/** /**
* A note entered on a prayer request * A note entered on a prayer request
*/ */
class Note class Note
{ {
use JsonSerialize;
/** /**
* @param string $asOf The date/time this note was recorded * @param string $asOf The date/time this note was recorded
* @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(#[Json] public string $asOf, #[Json] public string $text) { }
/** /**
* Retrieve notes for a given request * Retrieve notes for a given request
@ -24,7 +34,6 @@ class Note
*/ */
public static function byRequestId(string $id): array public static function byRequestId(string $id): array
{ {
$req = Request::byId($id); return Request::byId($id)->map(fn(Request $it) => Option::Some($it->notes))->getOrDefault([]);
return $req->isDefined() ? $req->get()->notes : [];
} }
} }

View File

@ -1,20 +1,53 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal\Domain; namespace MyPrayerJournal\Domain;
use BitBadger\InspiredByFSharp\Option;
use DateInterval; use DateInterval;
use JsonSerializable; use Square\Pjson\{Json, JsonSerialize};
/** /**
* The recurrence for a prayer request * The recurrence for a prayer request
*/ */
class Recurrence implements JsonSerializable class Recurrence
{ {
use JsonSerialize;
/** @var RecurrencePeriod The recurrence period */
#[Json]
public RecurrencePeriod $period {
get => $this->period ?? RecurrencePeriod::Immediate;
set { $this->period = $value; }
}
/** @var int|null How many of the periods will pass before the request is visible again */
#[Json('interval', omit_empty: true)]
private ?int $dbInterval {
get => $this->interval->unwrap();
set { $this->interval = Option::of($value); }
}
/** @var Option<int> How many of the periods will pass before the request is visible again */
public Option $interval {
get => $this->interval ?? Option::None();
set { $this->interval = $value; }
}
/** /**
* @param RecurrencePeriod $period The recurrence period * @param RecurrencePeriod $period The recurrence period
* @param int|null $interval How many of the periods will pass before the request is visible again * @param int|null $interval How many of the periods will pass before the request is visible again
*/ */
public function __construct(public RecurrencePeriod $period, public ?int $interval = null) { } public function __construct(RecurrencePeriod $period, ?int $interval = null)
{
$this->period = $period;
$this->dbInterval = $interval;
}
/** /**
* Get the date/time interval for this recurrence * Get the date/time interval for this recurrence
@ -25,17 +58,10 @@ class Recurrence implements JsonSerializable
{ {
$period = match ($this->period) { $period = match ($this->period) {
RecurrencePeriod::Immediate => 'T0S', RecurrencePeriod::Immediate => 'T0S',
RecurrencePeriod::Hours => "T{$this->interval}H", RecurrencePeriod::Hours => "T{$this->interval->value}H",
RecurrencePeriod::Days => "{$this->interval}D", RecurrencePeriod::Days => "{$this->interval->value}D",
RecurrencePeriod::Weeks => ($this->interval * 7) . 'D' RecurrencePeriod::Weeks => ($this->interval->value * 7) . 'D'
}; };
return new DateInterval("P$period"); return new DateInterval("P$period");
} }
public function jsonSerialize(): mixed
{
$values = ['period' => $this->period->value];
if (isset($this->interval)) $values['interval'] = $this->interval;
return $values;
}
} }

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal\Domain; namespace MyPrayerJournal\Domain;

View File

@ -1,77 +1,94 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal\Domain; namespace MyPrayerJournal\Domain;
use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\{Custom, DocumentException, DocumentList, Find}; use BitBadger\PDODocument\{Custom, DocumentException, DocumentList, Find};
use BitBadger\PDODocument\Mapper\DocumentMapper; use BitBadger\PDODocument\Mapper\DocumentMapper;
use DateTimeImmutable; use DateTimeImmutable;
use Exception; use Exception;
use JsonSerializable;
use MyPrayerJournal\Table; use MyPrayerJournal\Table;
use PhpOption\{None, Option}; use Square\Pjson\{Json, JsonSerialize};
/** /**
* A prayer request * A prayer request
*/ */
class Request implements JsonSerializable class Request
{ {
/** use JsonSerialize;
* @param string $id The ID for the request
* @param string $enteredOn The date/time this request was originally entered
* @param string $userId The ID of the user to whom this request belongs
* @param string|null $snoozedUntil The date/time the snooze expires for this request (null = not snoozed)
* @param string|null $showAfter The date/time the current recurrence period is over (null = immediate)
* @param Recurrence $recurrence The recurrence for this request
* @param History[] $history The history of this request
* @param Note[] $notes Notes regarding this request
* @throws Exception If the ID generation fails
*/
public function __construct(public string $id = '', public string $enteredOn = '', public string $userId = '',
public ?string $snoozedUntil = null, public ?string $showAfter = null,
public Recurrence $recurrence = new Recurrence(RecurrencePeriod::Immediate),
public array $history = [], public array $notes = []) { }
/** /** @var string The ID for the request */
* Get the current text for this request #[Json]
* public string $id = '';
* @return string The most recent text for the request
*/ /** @var string The date/time this request was originally entered */
public function currentText(): string #[Json]
{ public string $enteredOn = '';
foreach ($this->history as $hist) if (isset($hist->text)) return $hist->text;
return ''; /** @var string The ID of the user to whom this request belongs */
#[Json]
public string $userId = '';
/** @var string|null The date/time the snooze expires for this request (null = not snoozed) */
#[Json('snoozedUntil', omit_empty: true)]
private ?string $dbSnoozedUntil {
get => $this->snoozedUntil->unwrap();
set { $this->snoozedUntil = Option::of($value); }
} }
/** /** @var Option<string> The date/time the snooze expires for this request (None = not snoozed) */
* Get the date/time this request was last marked as prayed public Option $snoozedUntil {
* get => $this->snoozedUntil ?? Option::None();
* @return string|null The date/time this request was last marked as prayed set { $this->snoozedUntil = $value; }
*/
public function lastPrayed(): ?string
{
foreach ($this->history as $hist) if ($hist->action == RequestAction::Prayed) return $hist->asOf;
return null;
} }
/** /** @var string|null The date/time the current recurrence period is over (null = immediate) */
* Has this request been answered? #[Json(omit_empty: true)]
* public ?string $showAfter = null;
* @return bool True if the request is answered, false if not
*/ /** @var Recurrence The recurrence for this request */
public function isAnswered(): bool #[Json]
{ public Recurrence $recurrence {
return $this->history[0]->action == RequestAction::Answered; get => $this->recurrence ?? new Recurrence(RecurrencePeriod::Immediate);
set => $this->recurrence = $value;
} }
/** /** @var History[] The history of this request */
* Is this request currently snoozed? #[Json(type: History::class)]
* public array $history = [];
* @return bool True if the request is snoozed, false if not
* @throws Exception If the snoozed until date/time is not valid /** @param Note[] $notes Notes regarding this request */
*/ #[Json(type: Note::class)]
public function isSnoozed(): bool public array $notes = [];
{
return isset($this->snoozedUntil) && new DateTimeImmutable($this->snoozedUntil) > new DateTimeImmutable('now'); /** The current text for this request */
public string $currentText {
get => Option::of(array_find($this->history, fn(History $it) => $it->text->isSome))
->map(fn(History $it) => $it->text->value)
->getOrDefault('');
}
/** @var Option<string> The date/time this request was last marked as prayed */
public Option $lastPrayed {
get => Option::of(array_find($this->history, fn(History $it) => $it->action === RequestAction::Prayed))
->map(fn(History $it) => $it->asOf);
}
/** Has this request been answered? */
public bool $isAnswered {
get => $this->history[0]->action === RequestAction::Answered;
}
/** Is this request currently snoozed? */
public bool $isSnoozed {
get => $this->snoozedUntil->isSome
&& new DateTimeImmutable($this->snoozedUntil->value) > new DateTimeImmutable('now');
} }
/** /**
@ -80,28 +97,12 @@ class Request implements JsonSerializable
* @return bool True if the request is pending, false if not * @return bool True if the request is pending, false if not
* @throws Exception If the snoozed or show-after date/times are not valid * @throws Exception If the snoozed or show-after date/times are not valid
*/ */
public function isPending(): bool public bool $isPending {
{ get => !$this->isSnoozed
return !$this->isSnoozed()
&& isset($this->showAfter) && isset($this->showAfter)
&& new DateTimeImmutable($this->showAfter) > new DateTimeImmutable('now'); && new DateTimeImmutable($this->showAfter) > new DateTimeImmutable('now');
} }
public function jsonSerialize(): mixed
{
$values = [
'id' => $this->id,
'enteredOn' => $this->enteredOn,
'userId' => $this->userId,
'recurrence' => $this->recurrence,
'history' => $this->history,
'notes' => $this->notes
];
if (isset($this->snoozedUntil)) $values['snoozedUntil'] = $this->snoozedUntil;
if (isset($this->showAfter)) $values['showAfter'] = $this->showAfter;
return $values;
}
/** /**
* Find a request by its ID * Find a request by its ID
* *
@ -111,8 +112,8 @@ class Request implements JsonSerializable
*/ */
public static function byId(string $id): Option public static function byId(string $id): Option
{ {
$req = Find::byId(Table::REQUEST, $id, self::class); return Find::byId(Table::REQUEST, $id, self::class)
return ($req->getOrElse(new Request('x'))->userId == $_SESSION['user_id']) ? $req : None::create(); ->map(fn(Request $it) => $it->userId === $_SESSION['user_id'] ? $it : null);
} }
/** /**
@ -187,7 +188,7 @@ class Request implements JsonSerializable
*/ */
public static function answered(): DocumentList public static function answered(): DocumentList
{ {
return self::forUser(false); return self::forUser(active: false);
} }
/** /**

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal\Domain; namespace MyPrayerJournal\Domain;

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal; namespace MyPrayerJournal;
@ -8,5 +14,5 @@ namespace MyPrayerJournal;
class Table class Table
{ {
/** @var string The prayer request table used by myPrayerJournal */ /** @var string The prayer request table used by myPrayerJournal */
const REQUEST = 'request'; const string REQUEST = 'request';
} }

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal\UI; namespace MyPrayerJournal\UI;
@ -32,11 +38,13 @@ class Component
{ {
Layout::bareHead(); Layout::bareHead();
$reqs = Request::forJournal(); $reqs = Request::forJournal();
if ($reqs->hasItems()) { ?> 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 <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 hx-swap=outerHTML aria-label="Prayer Requests"><?php
$spacer = '<span>&nbsp;</span>'; $spacer = '<span>&nbsp;</span>';
foreach ($reqs->items() as /** @var Request $req */ $req) { ?> foreach ($reqs->items as /** @var Request $req */ $req) {
$lastPrayed = $req->lastPrayed;
$lastActivity = $lastPrayed->getOrDefault($req->history[0]->asOf); ?>
<div class=col> <div class=col>
<div class="card h-100"> <div class="card h-100">
<div class="card-header p-0 d-flex" role=toolbar> <div class="card-header p-0 d-flex" role=toolbar>
@ -55,13 +63,11 @@ class Component
title="Mark as Prayed"><?=self::icon('done');?></button> title="Mark as Prayed"><?=self::icon('done');?></button>
</div> </div>
<div class=card-body> <div class=card-body>
<p class=request-text><?=htmlentities($req->currentText());?> <p class=request-text><?=htmlentities($req->currentText);?>
</div> </div>
<div class="card-footer text-end text-muted px-1 py-0"> <div class="card-footer text-end text-muted px-1 py-0">
<em><?php <em>last <?=$lastPrayed->map(fn() => 'prayed')->getOrDefault('activity')?>
$lastPrayed = $req->lastPrayed(); <?=self::relativeDate($lastPrayed->getOrDefault($req->history[0]->asOf))?>
echo 'last ' . (is_null($lastPrayed) ? 'activity': 'prayed') . ' '
. self::relativeDate($lastPrayed ?? $req->history[0]->asOf); ?>
</em> </em>
</div> </div>
</div> </div>
@ -115,9 +121,9 @@ class Component
public static function relativeDate(string $date): string public static function relativeDate(string $date): string
{ {
$parsed = new DateTimeImmutable($date); $parsed = new DateTimeImmutable($date);
$inZone = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone'])); $iso = $parsed->format('c');
return sprintf('<span title="%s">%s</span>', date_format($inZone, 'l, F j, Y \a\t g:ia T'), $title = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone']))->format('l, F j, Y \a\t g:ia T');
RelativeDate::between('now', $parsed)); return "<relative-date-time title=\"$title\" interval=10000>$iso</relative-date-time>";
} }
/** /**
@ -135,26 +141,24 @@ class Component
<div class="list-group-item px-0 d-flex flex-row align-items-start" id=req-<?=$req->id?>><?php <div class="list-group-item px-0 d-flex flex-row align-items-start" id=req-<?=$req->id?>><?php
echo self::pageLink("/request/full?id=$req->id", self::icon('description'), echo self::pageLink("/request/full?id=$req->id", self::icon('description'),
['class' => $btnClass, 'title' => 'View Full Request']); ['class' => $btnClass, 'title' => 'View Full Request']);
if (!$req->isAnswered()) { if (!$req->isAnswered) {
echo self::pageLink("/request/edit?id=$req->id", self::icon('edit'), echo self::pageLink("/request/edit?id=$req->id", self::icon('edit'),
['class' => $btnClass, 'title' => 'Edit Request']); ['class' => $btnClass, 'title' => 'Edit Request']);
} }
if ($req->isSnoozed()) { if ($req->isSnoozed) {
echo $restoreBtn($req->id, 'cancel-snooze', 'Cancel Snooze'); echo $restoreBtn($req->id, 'cancel-snooze', 'Cancel Snooze');
} elseif ($req->isPending()) { } elseif ($req->isPending) {
echo $restoreBtn($req->id, 'show', 'Show Now'); echo $restoreBtn($req->id, 'show', 'Show Now');
} }
echo '<p class="request-text mb-0">' . $req->currentText(); echo '<p class="request-text mb-0">' . htmlentities($req->currentText);
if ($req->isSnoozed() || $req->isPending() || $req->isAnswered()) { ?> if ($req->isSnoozed || $req->isPending || $req->isAnswered) { ?>
<br> <br>
<small class=text-muted><em><?php <small class=text-muted><em><?php
if ($req->isSnoozed()) { echo match (true) {
echo 'Snooze expires ' . self::relativeDate($req->snoozedUntil); $req->isSnoozed => 'Snooze expires ' . self::relativeDate($req->snoozedUntil->value),
} elseif ($req->isPending()) { $req->isPending => 'Request appears next ' . self::relativeDate($req->showAfter),
echo 'Request appears next ' . self::relativeDate($req->showAfter); default => 'Answered ' . self::relativeDate($req->history[0]->asOf)
} else { };?>
echo 'Answered ' . self::relativeDate($req->history[0]->asOf);
} ?>
</em></small><?php </em></small><?php
} ?> } ?>
</div><?php </div><?php
@ -169,7 +173,7 @@ class Component
public static function requestList(DocumentList $reqs): void public static function requestList(DocumentList $reqs): void
{ {
echo '<div class=list-group>'; echo '<div class=list-group>';
foreach ($reqs->items() as $req) self::requestItem($req); $reqs->iter(self::requestItem(...));
echo '</div>'; echo '</div>';
} }
} }

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace MyPrayerJournal\UI; namespace MyPrayerJournal\UI;
@ -56,7 +62,7 @@ class Layout
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin=anonymous> crossorigin=anonymous>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel=stylesheet> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel=stylesheet>
<link href=/style/style.css rel=stylesheet> <link href=/_/style.css rel=stylesheet>
</head> </head>
HEAD; HEAD;
} }
@ -136,18 +142,19 @@ class Layout
rel=noopener>Developed</a> and hosted by rel=noopener>Developed</a> and hosted by
<a href=https://bitbadger.solutions target=_blank rel=noopener>Bit Badger Solutions</a> <a href=https://bitbadger.solutions target=_blank rel=noopener>Bit Badger Solutions</a>
</small></em> </small></em>
<script src=https://unpkg.com/htmx.org@2.0.0 crossorigin=anonymous <script src=https://unpkg.com/htmx.org@2.0.4 crossorigin=anonymous
integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw"></script> integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"></script>
<script>if (!htmx) document.write('<script src=\"/script/htmx.min.js\"><\/script>')</script> <script>if (!htmx) document.write('<script src=\"/_/htmx.min.js\"><\/script>')</script>
<script async src=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js <script async src=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin=anonymous></script> crossorigin=anonymous></script>
<script> <script>
setTimeout(function () { setTimeout(function () {
if (!bootstrap) document.write('<script src=\"/script/bootstrap.bundle.min.js\"><\/script>') if (!bootstrap) document.write('<script src=\"/_/bootstrap.bundle.min.js\"><\/script>')
}, 2000) }, 2000)
</script> </script>
<script src=/script/mpj.js></script> <script src=/_/mpj.js></script>
<script src=/_/relative-date-time.js defer></script>
</footer><?php </footer><?php
} }

View File

@ -1,66 +0,0 @@
<?php declare(strict_types=1);
namespace MyPrayerJournal\UI;
use DateTimeImmutable;
use Exception;
/**
* A type of relative date, along with the formatting strings
*/
enum RelativeDate: string
{
case LessThanXMinutes = 'less than a minute|less than %d minutes';
case XMinutes = 'a minute|%d minutes';
case AboutXHours = 'about an hour|about %d hours';
case XHours = 'an hour|%d hours';
case XDays = 'a day|%d days';
case AboutXWeeks = 'about a week|about %d weeks';
case XWeeks = 'a week|%d weeks';
case AboutXMonths = 'about a month|about %d months';
case XMonths = 'a month|%d months';
case AboutXYears = 'about a year|about %d years';
case XYears = 'a year|%d years';
case OverXYears = 'over a year|over %d years';
case AlmostXYears = 'almost a year|almost %d years';
// Many thanks to date-fns (https://date-fns.org) for this logic
/**
* Format the distance between two dates
*
* @param string|DateTimeImmutable $from The starting date/time
* @param string|DateTimeImmutable $to The ending date/time
* @return string The distance between two dates
* @throws Exception If date/time objects cannot be created
*/
public static function between(string|DateTimeImmutable $from, string|DateTimeImmutable $to): string
{
$aDay = 1_440.0;
$almost2Days = 2_520.0;
$aMonth = 43_200.0;
$twoMonths = 86_400.0;
$dtFrom = is_string($from) ? new DateTimeImmutable($from) : $from;
$dtTo = is_string($to) ? new DateTimeImmutable($to) : $to;
$minutes = abs($dtFrom->getTimestamp() - $dtTo->getTimestamp()) / 60;
$months = round($minutes / $aMonth);
$years = round($months / 12);
[$type, $number] = match (true) {
$minutes < 1.0 => [RelativeDate::LessThanXMinutes, 1],
$minutes < 45.0 => [RelativeDate::XMinutes, round($minutes)],
$minutes < 90.0 => [RelativeDate::AboutXHours, 1],
$minutes < $aDay => [RelativeDate::AboutXHours, round($minutes / 60)],
$minutes < $almost2Days => [RelativeDate::XDays, 1],
$minutes < $aMonth => [RelativeDate::XDays, round($minutes / $aDay)],
$minutes < $twoMonths => [RelativeDate::AboutXMonths, round($minutes / $aMonth)],
$months < 12 => [RelativeDate::XMonths, round($minutes / $aMonth)],
$months % 12 < 3 => [RelativeDate::AboutXYears, $years],
$months % 12 < 9 => [RelativeDate::OverXYears, $years],
default => [RelativeDate::AlmostXYears, $years]
};
[$singular, $plural] = explode('|', $type->value);
$value = $number == 1 ? $singular : sprintf($plural, $number);
return $dtFrom > $dtTo ? "$value ago" : "in $value";
}
}

1
src/public/_/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
"use strict" "use strict"
/** myPrayerJournal script */ /** myPrayerJournal script */
this.mpj = { window.mpj = {
/** /**
* Show a message via toast * Show a message via toast
* @param {string} message The message to show * @param {string} message The message to show

View File

@ -0,0 +1,121 @@
"use strict"
/**
* Relative Date/Time Custom HTML Element
*
* This creates an element that will take the existing date/time and replace it with words (ex. "about a year ago",
* "in 3 hours"). It will update based on the interval provided in the tag.
*
* ```html
* <relative-date-time interval=5000>2024-08-22T12:34:56+00:00</relative-date-time>
* ```
*/
class RelativeDateTime extends HTMLElement {
static #LessThanXMinutes = Symbol()
static #XMinutes = Symbol()
static #AboutXHours = Symbol()
static #XDays = Symbol()
static #AboutXMonths = Symbol()
static #XMonths = Symbol()
static #AboutXYears = Symbol()
static #OverXYears = Symbol()
static #AlmostXYears = Symbol()
static #messages = new Map([
[RelativeDateTime.#LessThanXMinutes, ['less than a minute', 'less than %d minutes']],
[RelativeDateTime.#XMinutes, ['a minute', '%d minutes']],
[RelativeDateTime.#AboutXHours, ['about an hour', 'about %d hours']],
[RelativeDateTime.#XDays, ['a day', '%d days']],
[RelativeDateTime.#AboutXMonths, ['about a month', 'about %d months']],
[RelativeDateTime.#XMonths, ['a month', '%d months']],
[RelativeDateTime.#AboutXYears, ['about a year', 'about %d years']],
[RelativeDateTime.#OverXYears, ['over a year', 'over %d years']],
[RelativeDateTime.#AlmostXYears, ['almost a year', 'almost %d years']],
])
static #aDay = 1440.0
static #almost2Days = 2520.0
static #aMonth = 43200.0
static #twoMonths = 86400.0
/**
* The date, parsed from the `innerHTML` of the element
* @type Date
*/
#jsDate
/**
* The ID of the interval (set via `setTimeout`, passed to `clearTimeout`)
* @type ?number
*/
#timeOut = null
constructor() {
super();
}
#update() {
const now = new Date()
const minutes = Math.abs((this.#jsDate.getTime() - now.getTime()) / 60 / 1000);
const months = Math.round(minutes / RelativeDateTime.#aMonth);
const years = Math.floor(months / 12);
/** @type symbol */
let typ
/** @type number */
let nbr
if (minutes < 1.0) {
typ = RelativeDateTime.#LessThanXMinutes
nbr = 1
} else if (minutes < 45.0) {
typ = RelativeDateTime.#XMinutes
nbr = Math.round(minutes)
} else if (minutes < 90.0) {
typ = RelativeDateTime.#AboutXHours
nbr = 1
} else if (minutes < RelativeDateTime.#aDay) {
typ = RelativeDateTime.#AboutXHours
nbr = Math.round(minutes / 60)
} else if (minutes < RelativeDateTime.#almost2Days) {
typ = RelativeDateTime.#XDays
nbr = 1
} else if (minutes < RelativeDateTime.#aMonth) {
typ = RelativeDateTime.#XDays
nbr = Math.round(minutes / RelativeDateTime.#aDay)
} else if (minutes < RelativeDateTime.#twoMonths) {
typ = RelativeDateTime.#AboutXMonths
nbr = Math.round(minutes / RelativeDateTime.#aMonth)
} else if (months < 12) {
typ = RelativeDateTime.#XMonths
nbr = Math.round(minutes / RelativeDateTime.#aMonth)
} else if (months % 12 < 3) {
typ = RelativeDateTime.#AboutXYears
nbr = years
} else if (months % 12 < 9) {
typ = RelativeDateTime.#OverXYears
nbr = years
} else {
typ = RelativeDateTime.#AlmostXYears
nbr = years + 1
}
const tmpl = RelativeDateTime.#messages.get(typ)
const message = nbr === 1 ? tmpl[0] : tmpl[1].replace("%d", nbr.toString())
this.innerText = this.#jsDate < now ? `${message} ago` : `in ${message}`
}
connectedCallback() {
this.#jsDate = new Date(this.innerText)
this.#update()
this.#timeOut = setInterval(() => this.#update(), parseInt(this.getAttribute("interval")))
}
disconnectedCallback() {
if (this.#timeOut) clearInterval(this.#timeOut)
}
}
customElements.define("relative-date-time", RelativeDateTime)

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Auth; use MyPrayerJournal\Auth;
use MyPrayerJournal\UI\Component; use MyPrayerJournal\UI\Component;

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\UI\Layout; use MyPrayerJournal\UI\Layout;

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\UI\{Component, Layout}; use MyPrayerJournal\UI\{Component, Layout};

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\UI\Layout; use MyPrayerJournal\UI\Layout;
@ -10,7 +16,7 @@ Layout::bareHead(); ?>
<form hx-patch="/request/snooze?id=<?=$req->id?>" hx-target=#journalItems hx-swap=outerHTML> <form hx-patch="/request/snooze?id=<?=$req->id?>" hx-target=#journalItems hx-swap=outerHTML>
<div class="form-floating pb-3"> <div class="form-floating pb-3">
<input type=date id=until name=until class=form-control <input type=date id=until name=until class=form-control
min="<?=(new DateTimeImmutable('now'))->format('Y-m-d')?>" required> min="<?=new DateTimeImmutable('now')->format('Y-m-d')?>" required>
<label for=until>Until</label> <label for=until>Until</label>
</div> </div>
<p class="text-end mb-0"><button type=submit class="btn btn-primary">Snooze</button> <p class="text-end mb-0"><button type=submit class="btn btn-primary">Snooze</button>

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\UI\Layout; use MyPrayerJournal\UI\Layout;

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\UI\Component;use MyPrayerJournal\UI\Layout; use MyPrayerJournal\UI\Component;use MyPrayerJournal\UI\Layout;

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Auth; use MyPrayerJournal\Auth;
use MyPrayerJournal\UI\{Component, Layout}; use MyPrayerJournal\UI\{Component, Layout};

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\UI\Layout; use MyPrayerJournal\UI\Layout;

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\UI\{Component, Layout}; use MyPrayerJournal\UI\{Component, Layout};

View File

@ -1,5 +1,12 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\RemoveFields; use BitBadger\PDODocument\RemoveFields;
use MyPrayerJournal\Table; use MyPrayerJournal\Table;
use MyPrayerJournal\UI\{Component, Layout}; use MyPrayerJournal\UI\{Component, Layout};
@ -9,7 +16,7 @@ require '../../start.php';
$req = validate_request($_GET['id'], ['PATCH'], false); $req = validate_request($_GET['id'], ['PATCH'], false);
RemoveFields::byId(Table::REQUEST, $req->id, ['snoozedUntil']); RemoveFields::byId(Table::REQUEST, $req->id, ['snoozedUntil']);
$req->snoozedUntil = null; $req->snoozedUntil = Option::None();
// TODO: message // TODO: message
Layout::bareHead(); Layout::bareHead();

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Auth; use MyPrayerJournal\Auth;
use MyPrayerJournal\Domain\{RecurrencePeriod, Request, RequestAction}; use MyPrayerJournal\Domain\{RecurrencePeriod, Request, RequestAction};
@ -31,7 +37,7 @@ Layout::pageHead("$action Prayer Request");?>
<input type=hidden name=returnTo value=<?=$cancelLink?>> <input type=hidden name=returnTo value=<?=$cancelLink?>>
<div class="form-floating pb-3"> <div class="form-floating pb-3">
<textarea id=requestText name=requestText class=form-control style="min-height: 8rem;" <textarea id=requestText name=requestText class=form-control style="min-height: 8rem;"
placeholder="Enter the text of the request" autofocus required><?=$req->currentText()?></textarea> placeholder="Enter the text of the request" autofocus required><?=$req->currentText?></textarea>
<label for=requestText>Prayer Request</label> <label for=requestText>Prayer Request</label>
</div><br><?php </div><br><?php
if (!$isNew) { ?> if (!$isNew) { ?>
@ -72,23 +78,23 @@ Layout::pageHead("$action Prayer Request");?>
</div> </div>
<div class="form-floating mx-2"> <div class="form-floating mx-2">
<input type=number class=form-control id=recurCount name=recurCount placeholder=0 required <input type=number class=form-control id=recurCount name=recurCount placeholder=0 required
value=<?=$req->recurrence->interval ?? 0?> style="width:6rem;"<?php value=<?=$req->recurrence->interval->getOrDefault(0)?> style="width:6rem;"<?php
if ($req->recurrence->period == RecurrencePeriod::Immediate) echo ' disabled'; ?>> if ($req->recurrence->period === RecurrencePeriod::Immediate) echo ' disabled'; ?>>
<label for=recurCount>Count</label> <label for=recurCount>Count</label>
</div> </div>
<div class="form-floating mx-2"> <div class="form-floating mx-2">
<select class=form-control id=recurInterval name=recurInterval style="width:6rem;" required<?php <select class=form-control id=recurInterval name=recurInterval style="width:6rem;" required<?php
if ($req->recurrence->period == RecurrencePeriod::Immediate) echo ' disabled'; ?>> if ($req->recurrence->period === RecurrencePeriod::Immediate) echo ' disabled'; ?>>
<option value=<?=RecurrencePeriod::Hours->value?><?php <option value=<?=RecurrencePeriod::Hours->value?><?php
if ($req->recurrence->period == RecurrencePeriod::Hours) echo ' selected'; ?>> if ($req->recurrence->period === RecurrencePeriod::Hours) echo ' selected'; ?>>
hours hours
</option> </option>
<option value=<?=RecurrencePeriod::Days->value?><?php <option value=<?=RecurrencePeriod::Days->value?><?php
if ($req->recurrence->period == RecurrencePeriod::Days) echo ' selected'; ?>> if ($req->recurrence->period === RecurrencePeriod::Days) echo ' selected'; ?>>
days days
</option> </option>
<option value=<?=RecurrencePeriod::Weeks->value?><?php <option value=<?=RecurrencePeriod::Weeks->value?><?php
if ($req->recurrence->period == RecurrencePeriod::Weeks) echo ' selected'; ?>> if ($req->recurrence->period === RecurrencePeriod::Weeks) echo ' selected'; ?>>
weeks weeks
</option> </option>
</select> </select>

View File

@ -1,13 +1,19 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Domain\{History, Note, RequestAction}; use MyPrayerJournal\Domain\{History, Note, RequestAction};
use MyPrayerJournal\UI\{Layout, RelativeDate}; use MyPrayerJournal\UI\{Component, Layout};
require '../../start.php'; require '../../start.php';
$req = validate_request($_GET['id'], ['GET']); $req = validate_request($_GET['id'], ['GET']);
$answered = $req->isAnswered() ? new DateTimeImmutable($req->history[0]->asOf) : null; $answered = $req->isAnswered ? new DateTimeImmutable($req->history[0]->asOf) : null;
$prayed = sizeof(array_filter($req->history, fn(History $hist) => $hist->action == RequestAction::Prayed)); $prayed = sizeof(array_filter($req->history, fn(History $hist) => $hist->action == RequestAction::Prayed));
$daysOpen = $daysOpen =
(($answered ?? new DateTimeImmutable('now'))->getTimestamp() (($answered ?? new DateTimeImmutable('now'))->getTimestamp()
@ -15,10 +21,10 @@ $daysOpen =
$logs = array_merge( $logs = array_merge(
array_map(fn(Note $note) => [new DateTimeImmutable($note->asOf), 'Notes', $note->text], $req->notes), 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 ?? ''], array_map(fn(History $hist) => [new DateTimeImmutable($hist->asOf), $hist->action->value, $hist->text->getOrDefault('')],
$req->history)); $req->history));
usort($logs, fn($a, $b) => $a[0] > $b[0] ? -1 : 1); usort($logs, fn($a, $b) => $a[0] > $b[0] ? -1 : 1);
if ($req->isAnswered()) array_shift($logs); if ($req->isAnswered) array_shift($logs);
Layout::pageHead('Full Request');?> Layout::pageHead('Full Request');?>
<article class="container mt-3"> <article class="container mt-3">
@ -28,11 +34,11 @@ Layout::pageHead('Full Request');?>
<h6 class="card-subtitle text-muted mb-2"><?php <h6 class="card-subtitle text-muted mb-2"><?php
if (!is_null($answered)) { ?> if (!is_null($answered)) { ?>
Answered <?=$answered->format('F j, Y')?> Answered <?=$answered->format('F j, Y')?>
(<?= RelativeDate::between('now', $req->history[0]->asOf);?>) &bull;<?php (<?=Component::relativeDate($req->history[0]->asOf)?>) &bull;<?php
} ?> } ?>
Prayed <?=number_format($prayed)?> times &bull; Open <?=number_format($daysOpen)?> days Prayed <?=number_format($prayed)?> times &bull; Open <?=number_format($daysOpen)?> days
</h6> </h6>
<p class=card-text><?=htmlentities($req->currentText())?> <p class=card-text><?=htmlentities($req->currentText)?>
</div> </div>
<ul class="list-group list-group-flush"><?php <ul class="list-group list-group-flush"><?php
foreach ($logs as $log) { ?> foreach ($logs as $log) { ?>

View File

@ -1,14 +1,20 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\PDODocument\Patch; use BitBadger\PDODocument\Patch;
use MyPrayerJournal\Domain\Note;
use MyPrayerJournal\Table; use MyPrayerJournal\Table;
use MyPrayerJournal\Domain\Note;
require '../../start.php'; require '../../start.php';
$req = validate_request($_GET['id'], ['POST'], false); $req = validate_request($_GET['id'], ['POST'], false);
array_unshift($req->notes, new Note((new DateTimeImmutable('now'))->format('c'), $_POST['notes'])); array_unshift($req->notes, new Note(new DateTimeImmutable('now')->format('c'), $_POST['notes']));
Patch::byId(Table::REQUEST, $req->id, ['notes' => $req->notes]); Patch::byId(Table::REQUEST, $req->id, ['notes' => $req->notes]);
hide_modal('notes'); hide_modal('notes');

View File

@ -1,8 +1,14 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\PDODocument\Patch; use BitBadger\PDODocument\Patch;
use MyPrayerJournal\Domain\{History, RecurrencePeriod, RequestAction};
use MyPrayerJournal\Table; use MyPrayerJournal\Table;
use MyPrayerJournal\Domain\{History, RecurrencePeriod, RequestAction};
use MyPrayerJournal\UI\Component; use MyPrayerJournal\UI\Component;
require '../../start.php'; require '../../start.php';

View File

@ -1,5 +1,12 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\{Document, Patch, RemoveFields}; use BitBadger\PDODocument\{Document, Patch, RemoveFields};
use MyPrayerJournal\{Auth, Table}; use MyPrayerJournal\{Auth, Table};
use MyPrayerJournal\Domain\{History, Recurrence, RecurrencePeriod, Request, RequestAction}; use MyPrayerJournal\Domain\{History, Recurrence, RecurrencePeriod, Request, RequestAction};
@ -12,16 +19,17 @@ Auth::requireUser(false);
$now = new DateTimeImmutable('now'); $now = new DateTimeImmutable('now');
$recurrence = new Recurrence(RecurrencePeriod::from($_POST['recurType'] ?? $_PATCH['recurType'])); $recurrence = new Recurrence(RecurrencePeriod::from($_POST['recurType'] ?? $_PATCH['recurType']));
if ($recurrence->period <> RecurrencePeriod::Immediate) { if ($recurrence->period <> RecurrencePeriod::Immediate) {
$recurrence->interval = (int)($_POST['recurCount'] ?? $_PATCH['recurCount']); $recurrence->interval = Option::of((int)($_POST['recurCount'] ?? $_PATCH['recurCount']));
} }
switch ($_SERVER['REQUEST_METHOD']) { switch ($_SERVER['REQUEST_METHOD']) {
case 'POST': case 'POST':
Document::insert(Table::REQUEST, new Request( $req = new Request();
enteredOn: $now->format('c'), $req->enteredOn = $now->format('c');
userId: $_SESSION['user_id'], $req->userId = $_SESSION['user_id'];
recurrence: $recurrence, $req->recurrence = $recurrence;
history: [new History($now->format('c'), RequestAction::Created, $_POST['requestText'])])); $req->history = [new History($now->format('c'), RequestAction::Created, $_POST['requestText'])];
Document::insert(Table::REQUEST, $req);
//Messages.pushSuccess ctx "Added prayer request" "/journal" //Messages.pushSuccess ctx "Added prayer request" "/journal"
see_other('/journal'); see_other('/journal');
@ -29,15 +37,15 @@ switch ($_SERVER['REQUEST_METHOD']) {
$req = Request::byId($_PATCH['requestId'])->getOrCall(not_found(...)); $req = Request::byId($_PATCH['requestId'])->getOrCall(not_found(...));
$patch = []; $patch = [];
// update recurrence if changed // update recurrence if changed
if ($recurrence != $req->recurrence) { if ($recurrence !== $req->recurrence) {
$patch['recurrence'] = $recurrence; $patch['recurrence'] = $recurrence;
if ($recurrence->period == RecurrencePeriod::Immediate) { if ($recurrence->period === RecurrencePeriod::Immediate) {
RemoveFields::byId(Table::REQUEST, $req->id, ['showAfter']); RemoveFields::byId(Table::REQUEST, $req->id, ['showAfter']);
} }
} }
// append history // append history
$upd8Text = trim($_PATCH['requestText']); $upd8Text = trim($_PATCH['requestText']);
$text = $upd8Text == '' || $upd8Text == $req->currentText() ? null : $upd8Text; $text = empty($upd8Text) || $upd8Text === $req->currentText ? null : $upd8Text;
array_unshift($req->history, new History($now->format('c'), RequestAction::from($_PATCH['status']), $text)); array_unshift($req->history, new History($now->format('c'), RequestAction::from($_PATCH['status']), $text));
$patch['history'] = $req->history; $patch['history'] = $req->history;
Patch::byId(Table::REQUEST, $req->id, $patch); Patch::byId(Table::REQUEST, $req->id, $patch);

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\PDODocument\RemoveFields; use BitBadger\PDODocument\RemoveFields;
use MyPrayerJournal\Table; use MyPrayerJournal\Table;

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\PDODocument\Patch; use BitBadger\PDODocument\Patch;
use MyPrayerJournal\Table; use MyPrayerJournal\Table;

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Auth; use MyPrayerJournal\Auth;
use MyPrayerJournal\Domain\Request; use MyPrayerJournal\Domain\Request;
@ -14,7 +20,7 @@ $reqs = Request::active();
Layout::pageHead('Active Requests'); ?> Layout::pageHead('Active Requests'); ?>
<article class="container mt-3"> <article class="container mt-3">
<h2 class=pb-3>Active Requests</h2><?php <h2 class=pb-3>Active Requests</h2><?php
if ($reqs->hasItems()) { if ($reqs->hasItems) {
Component::requestList($reqs); Component::requestList($reqs);
} else { } else {
Component::noResults('No Active Requests', '/journal', 'Return to your journal', Component::noResults('No Active Requests', '/journal', 'Return to your journal',

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Auth; use MyPrayerJournal\Auth;
use MyPrayerJournal\Domain\Request; use MyPrayerJournal\Domain\Request;
@ -14,7 +20,7 @@ $reqs = Request::answered();
Layout::pageHead('Answered Requests'); ?> Layout::pageHead('Answered Requests'); ?>
<article class="container mt-3"> <article class="container mt-3">
<h2 class=pb-3>Answered Requests</h2><?php <h2 class=pb-3>Answered Requests</h2><?php
if ($reqs->hasItems()) { if ($reqs->hasItems) {
Component::requestList($reqs); Component::requestList($reqs);
} else { } else {
Component::noResults('No Answered Requests', '/journal', 'Return to your journal', <<<'TEXT' Component::noResults('No Answered Requests', '/journal', 'Return to your journal', <<<'TEXT'

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Auth; use MyPrayerJournal\Auth;
use MyPrayerJournal\Domain\Request; use MyPrayerJournal\Domain\Request;
@ -14,7 +20,7 @@ $reqs = Request::snoozed();
Layout::pageHead('Snoozed Requests'); ?> Layout::pageHead('Snoozed Requests'); ?>
<article class="container mt-3"> <article class="container mt-3">
<h2 class=pb-3>Snoozed Requests</h2><?php <h2 class=pb-3>Snoozed Requests</h2><?php
if ($reqs->hasItems()) { if ($reqs->hasItems) {
Component::requestList($reqs); Component::requestList($reqs);
} else { } else {
Component::noResults('No Snoozed Requests', '/journal', 'Return to your journal', Component::noResults('No Snoozed Requests', '/journal', 'Return to your journal',

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Auth; use MyPrayerJournal\Auth;

View File

@ -1,8 +0,0 @@
<?php declare(strict_types=1);
use MyPrayerJournal\Auth;
require '../../start.php';
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
Auth::logOn();

View File

@ -0,0 +1,14 @@
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Auth;
require '../../../start.php';
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
Auth::logOn();

View File

@ -1,4 +1,10 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use MyPrayerJournal\Auth; use MyPrayerJournal\Auth;

View File

@ -1,7 +1,13 @@
<?php declare(strict_types=1); <?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use Auth0\SDK\Exception\ConfigurationException; use Auth0\SDK\Exception\ConfigurationException;
use BitBadger\PDODocument\{AutoId, Configuration, Definition, DocumentException, Mode}; use BitBadger\PDODocument\{AutoId, Configuration, Definition, DocumentException};
use Dotenv\Dotenv; use Dotenv\Dotenv;
use MyPrayerJournal\{Auth, Table}; use MyPrayerJournal\{Auth, Table};
use MyPrayerJournal\Domain\Request; use MyPrayerJournal\Domain\Request;
@ -9,7 +15,7 @@ use MyPrayerJournal\Domain\Request;
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
/** The version of this application */ /** The version of this application */
const MPJ_VERSION = '4.0.0-alpha1'; const MPJ_VERSION = '4.0.0-beta2';
(Dotenv::createImmutable(__DIR__))->load(); (Dotenv::createImmutable(__DIR__))->load();
@ -24,8 +30,7 @@ if (php_sapi_name() != 'cli') {
$_REQUEST['time_zone'] = $_SERVER['HTTP_X_TIME_ZONE'] ?? 'Etc/UTC'; $_REQUEST['time_zone'] = $_SERVER['HTTP_X_TIME_ZONE'] ?? 'Etc/UTC';
} }
Configuration::$pdoDSN = 'sqlite:' . implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', 'mpj.db']); Configuration::useDSN('sqlite:' . implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', 'mpj.db']));
Configuration::$mode = Mode::SQLite;
Configuration::$autoId = AutoId::RandomString; Configuration::$autoId = AutoId::RandomString;
Configuration::$idStringLength = 12; Configuration::$idStringLength = 12;
@ -33,7 +38,7 @@ Definition::ensureTable(Table::REQUEST);
Definition::ensureFieldIndex(Table::REQUEST, 'user', ['userId']); Definition::ensureFieldIndex(Table::REQUEST, 'user', ['userId']);
$_PATCH = []; $_PATCH = [];
if ($_SERVER['REQUEST_METHOD'] ?? '' == 'PATCH') parse_str(file_get_contents('php://input'), $_PATCH); if ($_SERVER['REQUEST_METHOD'] ?? '' === 'PATCH') parse_str(file_get_contents('php://input'), $_PATCH);
/** /**
* Return a 404 and exit * Return a 404 and exit
@ -73,12 +78,11 @@ function hide_modal(string $name): void
* @param array $methods The allowable HTTP methods * @param array $methods The allowable HTTP methods
* @param bool $redirect Whether to redirect not-logged-on users (optional, defaults to true) * @param bool $redirect Whether to redirect not-logged-on users (optional, defaults to true)
* @return Request The request (failures will not return) * @return Request The request (failures will not return)
* @throws ConfigurationException If any is encountered * @throws ConfigurationException|DocumentException If any is encountered
* @throws DocumentException If any is encountered
*/ */
function validate_request(string $id, array $methods, bool $redirect = true): Request function validate_request(string $id, array $methods, bool $redirect = true): Request
{ {
if (sizeof(array_filter($methods, fn($it) => $_SERVER['REQUEST_METHOD'] == $it)) == 0) not_found(); if (empty(array_filter($methods, fn($it) => $_SERVER['REQUEST_METHOD'] === $it))) not_found();
Auth::requireUser($redirect); Auth::requireUser($redirect);