Compare commits
5 Commits
4.0.0-alph
...
version4
Author | SHA1 | Date | |
---|---|---|---|
75680fae00 | |||
516a903565 | |||
52ec3f819c | |||
3bb4be3127 | |||
9110643383 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,3 +5,6 @@
|
||||
# PHP ignore files
|
||||
src/vendor
|
||||
src/.env
|
||||
|
||||
# databases
|
||||
src/data/*.db
|
||||
|
@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "bit-badger/my-prayer-journal",
|
||||
"minimum-stability": "beta",
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.4",
|
||||
"ext-pdo": "*",
|
||||
"ext-sqlite3": "*",
|
||||
"bit-badger/pdo-document": "^1",
|
||||
"auth0/auth0-php": "^8.11",
|
||||
"bit-badger/pdo-document": "^2",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"guzzlehttp/psr7": "^2.6",
|
||||
"http-interop/http-factory-guzzle": "^1.2",
|
||||
"auth0/auth0-php": "^8.11",
|
||||
"square/pjson": "^0.5",
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
},
|
||||
"autoload": {
|
||||
|
558
src/composer.lock
generated
558
src/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,14 +10,13 @@ require 'start.php';
|
||||
echo 'Retrieving v3 requests...' . PHP_EOL;
|
||||
|
||||
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());
|
||||
|
||||
echo 'Found ' . sizeof($reqs) . ' requests; migrating to v4...' . PHP_EOL;
|
||||
|
||||
Configuration::resetPDO();
|
||||
Configuration::$mode = Mode::SQLite;
|
||||
Configuration::$pdoDSN = 'sqlite:./data/mpj.db';
|
||||
Configuration::useDSN('sqlite:./data/mpj.db');
|
||||
Configuration::$autoId = AutoId::RandomString;
|
||||
Configuration::$idStringLength = 12;
|
||||
|
||||
@ -26,7 +25,7 @@ Definition::ensureTable(Table::REQUEST);
|
||||
/** Convert dates to the same format */
|
||||
function convertDate(string $date): string
|
||||
{
|
||||
return (new DateTimeImmutable($date))->format('c');
|
||||
return new DateTimeImmutable($date)->format('c');
|
||||
}
|
||||
|
||||
foreach ($reqs as $reqJson) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
use JsonSerializable;
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use Square\Pjson\{Json, JsonSerialize};
|
||||
|
||||
/**
|
||||
* 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 RequestAction $action The action taken for this history entry
|
||||
* @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 jsonSerialize(): mixed
|
||||
public function __construct(string $asOf, RequestAction $action, ?string $text = null)
|
||||
{
|
||||
$values = ['asOf' => $this->asOf, 'action' => $this->action->value];
|
||||
if (isset($this->text)) $values['text'] = $this->text;
|
||||
return $values;
|
||||
$this->asOf = $asOf;
|
||||
$this->action = $action;
|
||||
$this->dbText = $text;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use BitBadger\PDODocument\DocumentException;
|
||||
use Square\Pjson\{Json, JsonSerialize};
|
||||
|
||||
/**
|
||||
* A note entered on a prayer request
|
||||
*/
|
||||
class Note
|
||||
{
|
||||
use JsonSerialize;
|
||||
|
||||
/**
|
||||
* @param string $asOf The date/time this note was recorded
|
||||
* @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
|
||||
@ -24,7 +34,6 @@ class Note
|
||||
*/
|
||||
public static function byRequestId(string $id): array
|
||||
{
|
||||
$req = Request::byId($id);
|
||||
return $req->isDefined() ? $req->get()->notes : [];
|
||||
return Request::byId($id)->map(fn(Request $it) => Option::Some($it->notes))->getOrDefault([]);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use DateInterval;
|
||||
use JsonSerializable;
|
||||
use Square\Pjson\{Json, JsonSerialize};
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
@ -25,17 +58,10 @@ class Recurrence implements JsonSerializable
|
||||
{
|
||||
$period = match ($this->period) {
|
||||
RecurrencePeriod::Immediate => 'T0S',
|
||||
RecurrencePeriod::Hours => "T{$this->interval}H",
|
||||
RecurrencePeriod::Days => "{$this->interval}D",
|
||||
RecurrencePeriod::Weeks => ($this->interval * 7) . 'D'
|
||||
RecurrencePeriod::Hours => "T{$this->interval->value}H",
|
||||
RecurrencePeriod::Days => "{$this->interval->value}D",
|
||||
RecurrencePeriod::Weeks => ($this->interval->value * 7) . 'D'
|
||||
};
|
||||
return new DateInterval("P$period");
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
$values = ['period' => $this->period->value];
|
||||
if (isset($this->interval)) $values['interval'] = $this->interval;
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use BitBadger\PDODocument\{Custom, DocumentException, DocumentList, Find};
|
||||
use BitBadger\PDODocument\Mapper\DocumentMapper;
|
||||
use DateTimeImmutable;
|
||||
use Exception;
|
||||
use JsonSerializable;
|
||||
use MyPrayerJournal\Table;
|
||||
use PhpOption\{None, Option};
|
||||
use Square\Pjson\{Json, JsonSerialize};
|
||||
|
||||
/**
|
||||
* A prayer request
|
||||
*/
|
||||
class Request implements JsonSerializable
|
||||
class Request
|
||||
{
|
||||
/**
|
||||
* @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 = []) { }
|
||||
use JsonSerialize;
|
||||
|
||||
/**
|
||||
* Get the current text for this request
|
||||
*
|
||||
* @return string The most recent text for the request
|
||||
*/
|
||||
public function currentText(): string
|
||||
{
|
||||
foreach ($this->history as $hist) if (isset($hist->text)) return $hist->text;
|
||||
return '';
|
||||
/** @var string The ID for the request */
|
||||
#[Json]
|
||||
public string $id = '';
|
||||
|
||||
/** @var string The date/time this request was originally entered */
|
||||
#[Json]
|
||||
public string $enteredOn = '';
|
||||
|
||||
/** @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); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the date/time this request was last marked as prayed
|
||||
*
|
||||
* @return string|null The date/time this request was last marked as prayed
|
||||
*/
|
||||
public function lastPrayed(): ?string
|
||||
{
|
||||
foreach ($this->history as $hist) if ($hist->action == RequestAction::Prayed) return $hist->asOf;
|
||||
return null;
|
||||
/** @var Option<string> The date/time the snooze expires for this request (None = not snoozed) */
|
||||
public Option $snoozedUntil {
|
||||
get => $this->snoozedUntil ?? Option::None();
|
||||
set { $this->snoozedUntil = $value; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/** @var string|null The date/time the current recurrence period is over (null = immediate) */
|
||||
#[Json(omit_empty: true)]
|
||||
public ?string $showAfter = null;
|
||||
|
||||
/** @var Recurrence The recurrence for this request */
|
||||
#[Json]
|
||||
public Recurrence $recurrence {
|
||||
get => $this->recurrence ?? new Recurrence(RecurrencePeriod::Immediate);
|
||||
set => $this->recurrence = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this request currently snoozed?
|
||||
*
|
||||
* @return bool True if the request is snoozed, false if not
|
||||
* @throws Exception If the snoozed until date/time is not valid
|
||||
*/
|
||||
public function isSnoozed(): bool
|
||||
{
|
||||
return isset($this->snoozedUntil) && new DateTimeImmutable($this->snoozedUntil) > new DateTimeImmutable('now');
|
||||
/** @var History[] The history of this request */
|
||||
#[Json(type: History::class)]
|
||||
public array $history = [];
|
||||
|
||||
/** @param Note[] $notes Notes regarding this request */
|
||||
#[Json(type: Note::class)]
|
||||
public array $notes = [];
|
||||
|
||||
/** 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
|
||||
* @throws Exception If the snoozed or show-after date/times are not valid
|
||||
*/
|
||||
public function isPending(): bool
|
||||
{
|
||||
return !$this->isSnoozed()
|
||||
public bool $isPending {
|
||||
get => !$this->isSnoozed
|
||||
&& isset($this->showAfter)
|
||||
&& 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
|
||||
*
|
||||
@ -111,8 +112,8 @@ class Request implements JsonSerializable
|
||||
*/
|
||||
public static function byId(string $id): Option
|
||||
{
|
||||
$req = Find::byId(Table::REQUEST, $id, self::class);
|
||||
return ($req->getOrElse(new Request('x'))->userId == $_SESSION['user_id']) ? $req : None::create();
|
||||
return Find::byId(Table::REQUEST, $id, self::class)
|
||||
->map(fn(Request $it) => $it->userId === $_SESSION['user_id'] ? $it : null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -187,7 +188,7 @@ class Request implements JsonSerializable
|
||||
*/
|
||||
public static function answered(): DocumentList
|
||||
{
|
||||
return self::forUser(false);
|
||||
return self::forUser(active: false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
@ -8,5 +14,5 @@ namespace MyPrayerJournal;
|
||||
class Table
|
||||
{
|
||||
/** @var string The prayer request table used by myPrayerJournal */
|
||||
const REQUEST = 'request';
|
||||
const string REQUEST = 'request';
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
@ -32,11 +38,13 @@ class Component
|
||||
{
|
||||
Layout::bareHead();
|
||||
$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
|
||||
hx-swap=outerHTML aria-label="Prayer Requests"><?php
|
||||
$spacer = '<span> </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="card h-100">
|
||||
<div class="card-header p-0 d-flex" role=toolbar>
|
||||
@ -55,13 +63,11 @@ class Component
|
||||
title="Mark as Prayed"><?=self::icon('done');?></button>
|
||||
</div>
|
||||
<div class=card-body>
|
||||
<p class=request-text><?=htmlentities($req->currentText());?>
|
||||
<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>last <?=$lastPrayed->map(fn() => 'prayed')->getOrDefault('activity')?>
|
||||
<?=self::relativeDate($lastPrayed->getOrDefault($req->history[0]->asOf))?>
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
@ -115,9 +121,9 @@ class Component
|
||||
public static function relativeDate(string $date): string
|
||||
{
|
||||
$parsed = new DateTimeImmutable($date);
|
||||
$inZone = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone']));
|
||||
return sprintf('<span title="%s">%s</span>', date_format($inZone, 'l, F j, Y \a\t g:ia T'),
|
||||
RelativeDate::between('now', $parsed));
|
||||
$iso = $parsed->format('c');
|
||||
$title = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone']))->format('l, F j, Y \a\t g:ia T');
|
||||
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
|
||||
echo self::pageLink("/request/full?id=$req->id", self::icon('description'),
|
||||
['class' => $btnClass, 'title' => 'View Full Request']);
|
||||
if (!$req->isAnswered()) {
|
||||
if (!$req->isAnswered) {
|
||||
echo self::pageLink("/request/edit?id=$req->id", self::icon('edit'),
|
||||
['class' => $btnClass, 'title' => 'Edit Request']);
|
||||
}
|
||||
if ($req->isSnoozed()) {
|
||||
if ($req->isSnoozed) {
|
||||
echo $restoreBtn($req->id, 'cancel-snooze', 'Cancel Snooze');
|
||||
} elseif ($req->isPending()) {
|
||||
} elseif ($req->isPending) {
|
||||
echo $restoreBtn($req->id, 'show', 'Show Now');
|
||||
}
|
||||
echo '<p class="request-text mb-0">' . $req->currentText();
|
||||
if ($req->isSnoozed() || $req->isPending() || $req->isAnswered()) { ?>
|
||||
echo '<p class="request-text mb-0">' . htmlentities($req->currentText);
|
||||
if ($req->isSnoozed || $req->isPending || $req->isAnswered) { ?>
|
||||
<br>
|
||||
<small class=text-muted><em><?php
|
||||
if ($req->isSnoozed()) {
|
||||
echo 'Snooze expires ' . self::relativeDate($req->snoozedUntil);
|
||||
} elseif ($req->isPending()) {
|
||||
echo 'Request appears next ' . self::relativeDate($req->showAfter);
|
||||
} else {
|
||||
echo 'Answered ' . self::relativeDate($req->history[0]->asOf);
|
||||
} ?>
|
||||
echo match (true) {
|
||||
$req->isSnoozed => 'Snooze expires ' . self::relativeDate($req->snoozedUntil->value),
|
||||
$req->isPending => 'Request appears next ' . self::relativeDate($req->showAfter),
|
||||
default => 'Answered ' . self::relativeDate($req->history[0]->asOf)
|
||||
};?>
|
||||
</em></small><?php
|
||||
} ?>
|
||||
</div><?php
|
||||
@ -169,7 +173,7 @@ class Component
|
||||
public static function requestList(DocumentList $reqs): void
|
||||
{
|
||||
echo '<div class=list-group>';
|
||||
foreach ($reqs->items() as $req) self::requestItem($req);
|
||||
$reqs->iter(self::requestItem(...));
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
@ -56,7 +62,7 @@ class Layout
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
crossorigin=anonymous>
|
||||
<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;
|
||||
}
|
||||
@ -136,18 +142,19 @@ class Layout
|
||||
rel=noopener>Developed</a> and hosted by
|
||||
<a href=https://bitbadger.solutions target=_blank rel=noopener>Bit Badger Solutions</a>
|
||||
</small></em>
|
||||
<script src=https://unpkg.com/htmx.org@2.0.0 crossorigin=anonymous
|
||||
integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw"></script>
|
||||
<script>if (!htmx) document.write('<script src=\"/script/htmx.min.js\"><\/script>')</script>
|
||||
<script src=https://unpkg.com/htmx.org@2.0.4 crossorigin=anonymous
|
||||
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"></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
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin=anonymous></script>
|
||||
<script>
|
||||
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)
|
||||
</script>
|
||||
<script src=/script/mpj.js></script>
|
||||
<script src=/_/mpj.js></script>
|
||||
<script src=/_/relative-date-time.js defer></script>
|
||||
</footer><?php
|
||||
}
|
||||
|
||||
|
@ -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
1
src/public/_/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
||||
"use strict"
|
||||
|
||||
/** myPrayerJournal script */
|
||||
this.mpj = {
|
||||
window.mpj = {
|
||||
/**
|
||||
* Show a message via toast
|
||||
* @param {string} message The message to show
|
121
src/public/_/relative-date-time.js
Normal file
121
src/public/_/relative-date-time.js
Normal 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)
|
@ -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\UI\Component;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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};
|
||||
|
||||
|
@ -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;
|
||||
|
||||
@ -10,7 +16,7 @@ Layout::bareHead(); ?>
|
||||
<form hx-patch="/request/snooze?id=<?=$req->id?>" hx-target=#journalItems hx-swap=outerHTML>
|
||||
<div class="form-floating pb-3">
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-end mb-0"><button type=submit class="btn btn-primary">Snooze</button>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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\UI\{Component, Layout};
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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};
|
||||
|
||||
|
@ -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 MyPrayerJournal\Table;
|
||||
use MyPrayerJournal\UI\{Component, Layout};
|
||||
@ -9,7 +16,7 @@ require '../../start.php';
|
||||
$req = validate_request($_GET['id'], ['PATCH'], false);
|
||||
|
||||
RemoveFields::byId(Table::REQUEST, $req->id, ['snoozedUntil']);
|
||||
$req->snoozedUntil = null;
|
||||
$req->snoozedUntil = Option::None();
|
||||
|
||||
// TODO: message
|
||||
Layout::bareHead();
|
||||
|
@ -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\Domain\{RecurrencePeriod, Request, RequestAction};
|
||||
@ -31,7 +37,7 @@ Layout::pageHead("$action Prayer Request");?>
|
||||
<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>
|
||||
placeholder="Enter the text of the request" autofocus required><?=$req->currentText?></textarea>
|
||||
<label for=requestText>Prayer Request</label>
|
||||
</div><br><?php
|
||||
if (!$isNew) { ?>
|
||||
@ -72,23 +78,23 @@ Layout::pageHead("$action Prayer Request");?>
|
||||
</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'; ?>>
|
||||
value=<?=$req->recurrence->interval->getOrDefault(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'; ?>>
|
||||
if ($req->recurrence->period === RecurrencePeriod::Immediate) echo ' disabled'; ?>>
|
||||
<option value=<?=RecurrencePeriod::Hours->value?><?php
|
||||
if ($req->recurrence->period == RecurrencePeriod::Hours) echo ' selected'; ?>>
|
||||
if ($req->recurrence->period === RecurrencePeriod::Hours) echo ' selected'; ?>>
|
||||
hours
|
||||
</option>
|
||||
<option value=<?=RecurrencePeriod::Days->value?><?php
|
||||
if ($req->recurrence->period == RecurrencePeriod::Days) echo ' selected'; ?>>
|
||||
if ($req->recurrence->period === RecurrencePeriod::Days) echo ' selected'; ?>>
|
||||
days
|
||||
</option>
|
||||
<option value=<?=RecurrencePeriod::Weeks->value?><?php
|
||||
if ($req->recurrence->period == RecurrencePeriod::Weeks) echo ' selected'; ?>>
|
||||
if ($req->recurrence->period === RecurrencePeriod::Weeks) echo ' selected'; ?>>
|
||||
weeks
|
||||
</option>
|
||||
</select>
|
||||
|
@ -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\UI\{Layout, RelativeDate};
|
||||
use MyPrayerJournal\UI\{Component, Layout};
|
||||
|
||||
require '../../start.php';
|
||||
|
||||
$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));
|
||||
$daysOpen =
|
||||
(($answered ?? new DateTimeImmutable('now'))->getTimestamp()
|
||||
@ -15,10 +21,10 @@ $daysOpen =
|
||||
|
||||
$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 ?? ''],
|
||||
array_map(fn(History $hist) => [new DateTimeImmutable($hist->asOf), $hist->action->value, $hist->text->getOrDefault('')],
|
||||
$req->history));
|
||||
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');?>
|
||||
<article class="container mt-3">
|
||||
@ -28,11 +34,11 @@ Layout::pageHead('Full Request');?>
|
||||
<h6 class="card-subtitle text-muted mb-2"><?php
|
||||
if (!is_null($answered)) { ?>
|
||||
Answered <?=$answered->format('F j, Y')?>
|
||||
(<?= RelativeDate::between('now', $req->history[0]->asOf);?>) •<?php
|
||||
(<?=Component::relativeDate($req->history[0]->asOf)?>) •<?php
|
||||
} ?>
|
||||
Prayed <?=number_format($prayed)?> times • Open <?=number_format($daysOpen)?> days
|
||||
</h6>
|
||||
<p class=card-text><?=htmlentities($req->currentText())?>
|
||||
<p class=card-text><?=htmlentities($req->currentText)?>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush"><?php
|
||||
foreach ($logs as $log) { ?>
|
||||
|
@ -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 MyPrayerJournal\Domain\Note;
|
||||
use MyPrayerJournal\Table;
|
||||
use MyPrayerJournal\Domain\Note;
|
||||
|
||||
require '../../start.php';
|
||||
|
||||
$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]);
|
||||
|
||||
hide_modal('notes');
|
||||
|
@ -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 MyPrayerJournal\Domain\{History, RecurrencePeriod, RequestAction};
|
||||
use MyPrayerJournal\Table;
|
||||
use MyPrayerJournal\Domain\{History, RecurrencePeriod, RequestAction};
|
||||
use MyPrayerJournal\UI\Component;
|
||||
|
||||
require '../../start.php';
|
||||
|
@ -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 MyPrayerJournal\{Auth, Table};
|
||||
use MyPrayerJournal\Domain\{History, Recurrence, RecurrencePeriod, Request, RequestAction};
|
||||
@ -12,16 +19,17 @@ 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']);
|
||||
$recurrence->interval = Option::of((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'])]));
|
||||
$req = new Request();
|
||||
$req->enteredOn = $now->format('c');
|
||||
$req->userId = $_SESSION['user_id'];
|
||||
$req->recurrence = $recurrence;
|
||||
$req->history = [new History($now->format('c'), RequestAction::Created, $_POST['requestText'])];
|
||||
Document::insert(Table::REQUEST, $req);
|
||||
//Messages.pushSuccess ctx "Added prayer request" "/journal"
|
||||
see_other('/journal');
|
||||
|
||||
@ -29,15 +37,15 @@ switch ($_SERVER['REQUEST_METHOD']) {
|
||||
$req = Request::byId($_PATCH['requestId'])->getOrCall(not_found(...));
|
||||
$patch = [];
|
||||
// update recurrence if changed
|
||||
if ($recurrence != $req->recurrence) {
|
||||
if ($recurrence !== $req->recurrence) {
|
||||
$patch['recurrence'] = $recurrence;
|
||||
if ($recurrence->period == RecurrencePeriod::Immediate) {
|
||||
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;
|
||||
$text = empty($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);
|
||||
|
@ -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 MyPrayerJournal\Table;
|
||||
|
@ -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 MyPrayerJournal\Table;
|
||||
|
@ -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\Domain\Request;
|
||||
@ -14,7 +20,7 @@ $reqs = Request::active();
|
||||
Layout::pageHead('Active Requests'); ?>
|
||||
<article class="container mt-3">
|
||||
<h2 class=pb-3>Active Requests</h2><?php
|
||||
if ($reqs->hasItems()) {
|
||||
if ($reqs->hasItems) {
|
||||
Component::requestList($reqs);
|
||||
} else {
|
||||
Component::noResults('No Active Requests', '/journal', 'Return to your journal',
|
||||
|
@ -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\Domain\Request;
|
||||
@ -14,7 +20,7 @@ $reqs = Request::answered();
|
||||
Layout::pageHead('Answered Requests'); ?>
|
||||
<article class="container mt-3">
|
||||
<h2 class=pb-3>Answered Requests</h2><?php
|
||||
if ($reqs->hasItems()) {
|
||||
if ($reqs->hasItems) {
|
||||
Component::requestList($reqs);
|
||||
} else {
|
||||
Component::noResults('No Answered Requests', '/journal', 'Return to your journal', <<<'TEXT'
|
||||
|
@ -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\Domain\Request;
|
||||
@ -14,7 +20,7 @@ $reqs = Request::snoozed();
|
||||
Layout::pageHead('Snoozed Requests'); ?>
|
||||
<article class="container mt-3">
|
||||
<h2 class=pb-3>Snoozed Requests</h2><?php
|
||||
if ($reqs->hasItems()) {
|
||||
if ($reqs->hasItems) {
|
||||
Component::requestList($reqs);
|
||||
} else {
|
||||
Component::noResults('No Snoozed Requests', '/journal', 'Return to your journal',
|
||||
|
1
src/public/script/htmx.min.js
vendored
1
src/public/script/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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;
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use MyPrayerJournal\Auth;
|
||||
|
||||
require '../../start.php';
|
||||
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
|
||||
|
||||
Auth::logOn();
|
14
src/public/user/log-on/index.php
Normal file
14
src/public/user/log-on/index.php
Normal 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();
|
@ -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;
|
||||
|
||||
|
@ -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 BitBadger\PDODocument\{AutoId, Configuration, Definition, DocumentException, Mode};
|
||||
use BitBadger\PDODocument\{AutoId, Configuration, Definition, DocumentException};
|
||||
use Dotenv\Dotenv;
|
||||
use MyPrayerJournal\{Auth, Table};
|
||||
use MyPrayerJournal\Domain\Request;
|
||||
@ -9,7 +15,7 @@ use MyPrayerJournal\Domain\Request;
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
/** The version of this application */
|
||||
const MPJ_VERSION = '4.0.0-alpha1';
|
||||
const MPJ_VERSION = '4.0.0-beta2';
|
||||
|
||||
(Dotenv::createImmutable(__DIR__))->load();
|
||||
|
||||
@ -24,8 +30,7 @@ if (php_sapi_name() != 'cli') {
|
||||
$_REQUEST['time_zone'] = $_SERVER['HTTP_X_TIME_ZONE'] ?? 'Etc/UTC';
|
||||
}
|
||||
|
||||
Configuration::$pdoDSN = 'sqlite:' . implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', 'mpj.db']);
|
||||
Configuration::$mode = Mode::SQLite;
|
||||
Configuration::useDSN('sqlite:' . implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', 'mpj.db']));
|
||||
Configuration::$autoId = AutoId::RandomString;
|
||||
Configuration::$idStringLength = 12;
|
||||
|
||||
@ -33,7 +38,7 @@ Definition::ensureTable(Table::REQUEST);
|
||||
Definition::ensureFieldIndex(Table::REQUEST, 'user', ['userId']);
|
||||
|
||||
$_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
|
||||
@ -73,12 +78,11 @@ function hide_modal(string $name): void
|
||||
* @param array $methods The allowable HTTP methods
|
||||
* @param bool $redirect Whether to redirect not-logged-on users (optional, defaults to true)
|
||||
* @return Request The request (failures will not return)
|
||||
* @throws ConfigurationException If any is encountered
|
||||
* @throws DocumentException If any is encountered
|
||||
* @throws ConfigurationException|DocumentException If any is encountered
|
||||
*/
|
||||
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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user