Replace PHP WIP with vanilla version
This commit is contained in:
parent
b0bf2cb083
commit
5f425adc1d
@ -1,4 +0,0 @@
|
||||
RewriteEngine on
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule . index.php [L]
|
@ -1,121 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
use Auth0\SDK\Auth0;
|
||||
|
||||
class AppUser
|
||||
{
|
||||
/** The Auth0 client instance to use for authentication */
|
||||
private static ?Auth0 $auth0 = null;
|
||||
|
||||
/**
|
||||
* Get the Auth0 instance
|
||||
*
|
||||
* @return Auth0 The Auth0 instance, lazily initialized
|
||||
*/
|
||||
private static function auth0Instance(): Auth0
|
||||
{
|
||||
if (is_null(self::$auth0)) {
|
||||
self::$auth0 = new \Auth0\SDK\Auth0([
|
||||
'domain' => $_ENV['AUTH0_DOMAIN'],
|
||||
'clientId' => $_ENV['AUTH0_CLIENT_ID'],
|
||||
'clientSecret' => $_ENV['AUTH0_CLIENT_SECRET'],
|
||||
'cookieSecret' => $_ENV['AUTH0_COOKIE_SECRET']
|
||||
]);
|
||||
}
|
||||
return self::$auth0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the host to use for return URLs
|
||||
*
|
||||
* @return string The host for return URLs
|
||||
*/
|
||||
private static function host()
|
||||
{
|
||||
return 'http' . ($_SERVER['SERVER_PORT'] == 443 ? 's' : '' ) . "://{$_SERVER['HTTP_HOST']}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the log on callback URL
|
||||
*
|
||||
* @return string The log on callback URL
|
||||
*/
|
||||
private static function logOnCallback()
|
||||
{
|
||||
return self::host() . '/user/log-on/success';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate a redirect to the Auth0 log on page
|
||||
*
|
||||
* @param string $nextUrl The URL (other than /journal) to which the user should be redirected
|
||||
* @return never This function exits the currently running script
|
||||
*/
|
||||
public static function logOn(?string $nextUrl = null): never
|
||||
{
|
||||
// TODO: pass the next URL in the Auth0 callback
|
||||
self::auth0Instance()->clear();
|
||||
header('Location: ' . self::auth0Instance()->login(self::logOnCallback()));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the log on response from Auth0
|
||||
*
|
||||
* @return never This function exits the currently running script
|
||||
*/
|
||||
public static function processLogOn(): never
|
||||
{
|
||||
self::auth0Instance()->exchange(self::logOnCallback());
|
||||
// TODO: check for next URL and redirect if present
|
||||
header('Location: /journal');
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log off the current user
|
||||
*
|
||||
* @return never This function exits the currently running script
|
||||
*/
|
||||
public static function logOff(): never
|
||||
{
|
||||
header('Location: ' . self::auth0Instance()->logout(self::host() . '/'));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
*
|
||||
* @return ?object The current user, or null if one is not signed in
|
||||
*/
|
||||
public static function current(): ?object
|
||||
{
|
||||
return self::auth0Instance()->getCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that there be a user logged on
|
||||
*
|
||||
* @return void This will not return if there is not a user logged on
|
||||
*/
|
||||
public static function require()
|
||||
{
|
||||
if (is_null(self::current())) {
|
||||
// TODO: get the current URL to specify for redirection
|
||||
self::logOn();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID (`sub`) for the current user
|
||||
*
|
||||
* @return string The ID of the user (blank string if there is no current user)
|
||||
*/
|
||||
public static function currentId(): string
|
||||
{
|
||||
return self::auth0Instance()->getCredentials()?->user['sub'] ?? '';
|
||||
}
|
||||
}
|
153
src/app/Data.php
153
src/app/Data.php
@ -1,153 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
use BitBadger\PgSQL\Documents\{ Configuration, Definition, Document, DocumentIndex, Query };
|
||||
use MyPrayerJournal\Domain\{ AsOf, History, JournalRequest, Note, Request, RequestAction };
|
||||
|
||||
class Data
|
||||
{
|
||||
/** The prayer request table */
|
||||
const REQ_TABLE = 'prayer_request';
|
||||
|
||||
/**
|
||||
* Configure the data connection
|
||||
*/
|
||||
public static function configure()
|
||||
{
|
||||
Configuration::$startUp = '\MyPrayerJournal\Data::startUp';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the table and index exist
|
||||
*/
|
||||
public static function startUp()
|
||||
{
|
||||
Definition::ensureTable(self::REQ_TABLE);
|
||||
Definition::ensureIndex(self::REQ_TABLE, DocumentIndex::Optimized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a full prayer request by its ID
|
||||
*
|
||||
* @param string $reqId The request ID
|
||||
* @param string $userId The ID of the currently logged-on user
|
||||
* @return ?Request The request, or null if it is not found
|
||||
*/
|
||||
public static function findFullRequestById(string $reqId, string $userId): ?Request
|
||||
{
|
||||
$req = Document::findById(self::REQ_TABLE, $reqId, Request::class);
|
||||
return is_null($req) || $req->userId != $userId ? null : $req;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a history entry to the specified request
|
||||
*
|
||||
* @param string $reqId The request ID
|
||||
* @param string $userId The ID of the currently logged-on user
|
||||
* @param History $history The history entry to be added
|
||||
*/
|
||||
public static function addHistory(string $reqId, string $userId, History $history)
|
||||
{
|
||||
$req = self::findFullRequestById($reqId, $userId);
|
||||
if (is_null($req)) throw new \InvalidArgumentException("$reqId not found");
|
||||
array_unshift($req->history, $history);
|
||||
Document::updateFull(self::REQ_TABLE, $reqId, $req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note to the specified request
|
||||
*
|
||||
* @param string $reqId The request ID
|
||||
* @param string $userId The ID of the currently logged-on user
|
||||
* @param Note $note The note to be added
|
||||
*/
|
||||
public static function addNote(string $reqId, string $userId, Note $note)
|
||||
{
|
||||
$req = self::findFullRequestById($reqId, $userId);
|
||||
if (is_null($req)) throw new \InvalidArgumentException("$reqId not found");
|
||||
array_unshift($req->notes, $note);
|
||||
Document::updateFull(self::REQ_TABLE, $reqId, $req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new request
|
||||
*
|
||||
* @param Request $req The request to be added
|
||||
*/
|
||||
public static function addRequest(Request $req)
|
||||
{
|
||||
Document::insert(self::REQ_TABLE, $req->id, $req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an array of `Request`s to an array of `JournalRequest`s
|
||||
*
|
||||
* @param Request[] $reqs The requests to map
|
||||
* @param bool $full Whether to include history and notes (true) or not (false)
|
||||
* @return JournalRequest[] The journal request objects
|
||||
*/
|
||||
private static function mapToJournalRequest(array $reqs, bool $full): array
|
||||
{
|
||||
return array_map(fn (Request $req) => new JournalRequest($req, $full), $reqs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get journal requests for the given user by "answered" status
|
||||
*
|
||||
* @param string $userId The ID of the user for whom requests should be retrieved
|
||||
* @param string $op The JSON Path operator to use for comparison (`==` or `<>`)
|
||||
* @return JournalRequest[] The journal request objects
|
||||
*/
|
||||
private static function getJournalByAnswered(string $userId, string $op): array
|
||||
{
|
||||
$isAnswered = str_replace(':path',
|
||||
"'$.history[*].action ? (@ $op \"" . RequestAction::Answered->name . "\")'",
|
||||
Query::whereJsonPathMatches(':path'));
|
||||
$sql = sprintf("%s WHERE %s AND $isAnswered", Query::selectFromTable(self::REQ_TABLE),
|
||||
Query::whereDataContains(':criteria'));
|
||||
$params = [ ':criteria' => Query::jsonbDocParam([ 'userId' => $userId ]) ];
|
||||
return self::mapToJournalRequest(
|
||||
Document::customList($sql, $params, Request::class, Document::mapFromJson(...)), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all answered requests for this user
|
||||
*
|
||||
* @param string $userId The ID of the user for whom answered requests should be retrieved
|
||||
* @return JournalRequest[] The answered requests
|
||||
*/
|
||||
public static function getAnsweredRequests(string $userId): array
|
||||
{
|
||||
$answered = self::getJournalByAnswered($userId, '==');
|
||||
usort($answered, AsOf::newestToOldest(...));
|
||||
return $answered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's current prayer request journal
|
||||
*
|
||||
* @param string $userId The ID of the user whose journal should be retrieved
|
||||
* @return JournalRequest[] The journal request objects
|
||||
*/
|
||||
public static function getJournal(string $userId): array
|
||||
{
|
||||
$reqs = self::getJournalByAnswered($userId, '<>');
|
||||
usort($reqs, AsOf::oldestToNewest(...));
|
||||
return $reqs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to obtain a journal request by its ID
|
||||
*
|
||||
* @param string $reqId The request ID
|
||||
* @param string $userId The ID of the currently logged-on user
|
||||
* @return ?JournalRequest The request, or null if it is not found
|
||||
*/
|
||||
public static function tryJournalById(string $reqId, string $userId): ?JournalRequest
|
||||
{
|
||||
$req = self::findFullRequestById($reqId, $userId);
|
||||
return is_null($req) ? null : new JournalRequest($req);
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
/**
|
||||
* The different distance formats supported
|
||||
*/
|
||||
enum DistanceFormat
|
||||
{
|
||||
case LessThanXMinutes;
|
||||
case XMinutes;
|
||||
case AboutXHours;
|
||||
case XHours;
|
||||
case XDays;
|
||||
case AboutXWeeks;
|
||||
case XWeeks;
|
||||
case AboutXMonths;
|
||||
case XMonths;
|
||||
case AboutXYears;
|
||||
case XYears;
|
||||
case OverXYears;
|
||||
case AlmostXYears;
|
||||
|
||||
/**
|
||||
* Return the formatting string for the given format and number
|
||||
*
|
||||
* @param DistanceFormat $it The distance format
|
||||
* @param bool $singular If true, returns the singular version; if false (default), returns the plural version
|
||||
* @return string The format string
|
||||
*/
|
||||
public static function format(DistanceFormat $it, bool $singular = false): string
|
||||
{
|
||||
return match ($it) {
|
||||
DistanceFormat::LessThanXMinutes => $singular ? 'less than a minute' : 'less than %i minutes',
|
||||
DistanceFormat::XMinutes => $singular ? 'a minute' : '%i minutes',
|
||||
DistanceFormat::AboutXHours => $singular ? 'about an hour' : 'about %i hours',
|
||||
DistanceFormat::XHours => $singular ? 'an hour' : '%i hours',
|
||||
DistanceFormat::XDays => $singular ? 'a day' : '%i days',
|
||||
DistanceFormat::AboutXWeeks => $singular ? 'about a week' : 'about %i weeks',
|
||||
DistanceFormat::XWeeks => $singular ? 'a week' : '%i weeks',
|
||||
DistanceFormat::AboutXMonths => $singular ? 'about a month' : 'about %i months',
|
||||
DistanceFormat::XMonths => $singular ? 'a month' : '%i months',
|
||||
DistanceFormat::AboutXYears => $singular ? 'about a year' : 'about %i years',
|
||||
DistanceFormat::XYears => $singular ? 'a year' : '%i years',
|
||||
DistanceFormat::OverXYears => $singular ? 'over a year' : 'over %i years',
|
||||
DistanceFormat::AlmostXYears => $singular ? 'almost a year' : 'almost %i years',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Dates
|
||||
{
|
||||
/** Minutes in a day */
|
||||
private const A_DAY = 1_440;
|
||||
|
||||
/** Minutes in two days(-ish) */
|
||||
private const ALMOST_2_DAYS = 2_520;
|
||||
|
||||
/** Minutes in a month */
|
||||
private const A_MONTH = 43_200;
|
||||
|
||||
/** Minutes in two months */
|
||||
private const TWO_MONTHS = 86_400;
|
||||
|
||||
/**
|
||||
* Get a UTC-referenced current date/time
|
||||
*
|
||||
* @return \DateTimeImmutable The current date/time with UTC reference
|
||||
*/
|
||||
public static function now(): \DateTimeImmutable
|
||||
{
|
||||
return new \DateTimeImmutable(timezone: new \DateTimeZone('Etc/UTC'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the distance between two instants in approximate English terms
|
||||
*
|
||||
* @param \DateTimeInterface $startOn The starting date/time for the comparison
|
||||
* @param \DateTimeInterface $endOn THe ending date/time for the comparison
|
||||
* @return string The formatted interval
|
||||
*/
|
||||
public static function formatDistance(\DateTimeInterface $startOn, \DateTimeInterface $endOn): string
|
||||
{
|
||||
$diff = $startOn->diff($endOn);
|
||||
$minutes =
|
||||
$diff->i + ($diff->h * 60) + ($diff->d * 60 * 24) + ($diff->m * 60 * 24 * 30) + ($diff->y * 60 * 24 * 365);
|
||||
$months = round($minutes / self::A_MONTH);
|
||||
$years = $months / 12;
|
||||
[ $format, $number ] = match (true) {
|
||||
$minutes < 1 => [ DistanceFormat::LessThanXMinutes, 1 ],
|
||||
$minutes < 45 => [ DistanceFormat::XMinutes, $minutes ],
|
||||
$minutes < 90 => [ DistanceFormat::AboutXHours, 1 ],
|
||||
$minutes < self::A_DAY => [ DistanceFormat::AboutXHours, round($minutes / 60) ],
|
||||
$minutes < self::ALMOST_2_DAYS => [ DistanceFormat::XDays, 1 ],
|
||||
$minutes < self::A_MONTH => [ DistanceFormat::XDays, round($minutes / self::A_DAY) ],
|
||||
$minutes < self::TWO_MONTHS => [ DistanceFormat::AboutXMonths, round($minutes / self::A_MONTH) ],
|
||||
$months < 12 => [ DistanceFormat::XMonths, round($minutes / self::A_MONTH) ],
|
||||
$months % 12 < 3 => [ DistanceFormat::AboutXYears, $years ],
|
||||
$months % 12 < 9 => [ DistanceFormat::OverXYears, $years ],
|
||||
default => [ DistanceFormat::AlmostXYears, $years + 1 ],
|
||||
};
|
||||
|
||||
$relativeWords = sprintf(DistanceFormat::format($format, $number == 1), $number);
|
||||
return $startOn > $endOn ? "$relativeWords ago" : "in $relativeWords";
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
use MyPrayerJournal\Domain\JournalRequest;
|
||||
|
||||
class Handlers
|
||||
{
|
||||
/**
|
||||
* Render a BareUI template
|
||||
*
|
||||
* @param string $template The template name to render
|
||||
* @param string $pageTitle The title for the page
|
||||
* @param ?array $params Parameters to use to render the page (optional)
|
||||
*/
|
||||
public static function render(string $template, string $pageTitle, ?array $params = null)
|
||||
{
|
||||
$params = array_merge($params ?? [], [
|
||||
'pageTitle' => $pageTitle,
|
||||
'isHtmx' =>
|
||||
array_key_exists('HTTP_HX_REQUEST', $_SERVER)
|
||||
&& (!array_key_exists('HTTP_HX_HISTORY_RESTORE_REQUEST', $_SERVER)),
|
||||
'user' => AppUser::current(),
|
||||
'hasSnoozed' => false,
|
||||
]);
|
||||
$params['pageContent'] = app()->template->render($template, $params);
|
||||
$layout = $params['isHtmx'] ? 'layout/partial' : 'layout/full';
|
||||
response()->markup(app()->template->render($layout, $params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a BareUI component template
|
||||
*
|
||||
* @param string $template The template name to render
|
||||
* @param ?array $params Parameter to use to render the component (optional)
|
||||
*/
|
||||
private static function renderComponent(string $template, ?array $params = null)
|
||||
{
|
||||
$params = $params ?? [];
|
||||
$params['pageContent'] = app()->template->render($template, $params);
|
||||
header('Cache-Control: no-cache, max-age=-1');
|
||||
response()->markup(app()->template->render('layout/component', $params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a 404 Not Found response
|
||||
*/
|
||||
private static function notFound()
|
||||
{
|
||||
response()->plain('Not found', 404);
|
||||
}
|
||||
|
||||
/** GET: /journal */
|
||||
public static function journal()
|
||||
{
|
||||
AppUser::require();
|
||||
|
||||
$user = AppUser::current()->user;
|
||||
$firstName = (array_key_exists('given_name', $user) ? $user['given_name'] : null) ?? 'Your';
|
||||
self::render('journal', $firstName . ($firstName == 'Your' ? '' : '’s') . ' Prayer Journal');
|
||||
}
|
||||
|
||||
/** GET: /components/journal-items */
|
||||
public static function journalItems()
|
||||
{
|
||||
AppUser::require();
|
||||
|
||||
$reqs = Data::getJournal(AppUser::currentId());
|
||||
$utc = new \DateTimeZone('Etc/UTC');
|
||||
$now = date_create_immutable(timezone: $utc);
|
||||
$epoch = date_create_immutable('1970-01-01', $utc);
|
||||
array_filter($reqs,
|
||||
fn (JournalRequest $req) => $req->snoozedUntil ?? $epoch < $now && $req->showAfter ?? $epoch < $now);
|
||||
|
||||
self::renderComponent('components/journal_items', [ 'requests' => $reqs ]);
|
||||
}
|
||||
|
||||
/** GET /request/[req-id]/edit */
|
||||
public static function requestEdit(string $reqId)
|
||||
{
|
||||
AppUser::require();
|
||||
|
||||
$returnTo = array_key_exists('HTTP_REFERER', $_SERVER)
|
||||
? match (true) {
|
||||
str_ends_with($_SERVER['HTTP_REFERER'], '/active') => 'active',
|
||||
str_ends_with($_SERVER['HTTP_REFERER'], '/snoozed') => 'snoozed',
|
||||
default => 'journal'
|
||||
}
|
||||
: 'journal';
|
||||
if ($reqId == 'new') {
|
||||
self::render('requests/edit', 'Add Prayer Request', [
|
||||
'request' => new JournalRequest(),
|
||||
'isNew' => true,
|
||||
'returnTo' => $returnTo,
|
||||
]);
|
||||
} else {
|
||||
$req = Data::tryJournalById($reqId, AppUser::currentId());
|
||||
if (is_null($req)) {
|
||||
self::notFound();
|
||||
} else {
|
||||
self::render('requests/edit', 'Edit Prayer Request', [
|
||||
'request' => $req,
|
||||
'isNew' => false,
|
||||
'returnTo' => $returnTo,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** POST|PATCH /request */
|
||||
public static function requestSave()
|
||||
{
|
||||
AppUser::require();
|
||||
|
||||
$form = request()->validate([
|
||||
'requestId' => ['required', 'text'],
|
||||
'requestText' => ['required', 'text'],
|
||||
'status' => ['required', 'textOnly'],
|
||||
'recurType' => ['required', 'textOnly'],
|
||||
'recurCount' => 'number',
|
||||
'recurInterval' => 'textOnly',
|
||||
]);
|
||||
if ($form) {
|
||||
// valid
|
||||
} else {
|
||||
// errors
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +1,29 @@
|
||||
{
|
||||
"name": "bitbadger/my-prayer-journal",
|
||||
"description": "Minimalist prayer journal to enhance your prayer life",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MyPrayerJournal\\": "lib/",
|
||||
"MyPrayerJournal\\Domain\\": "lib/domain/",
|
||||
"BitBadger\\PgDocuments\\": "lib/documents/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Daniel J. Summers",
|
||||
"email": "daniel@bitbadger.solutions"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"leafs/leaf": "^3.0",
|
||||
"leafs/bareui": "^1.1",
|
||||
"leafs/db": "^2.1",
|
||||
"netresearch/jsonmapper": "^4.2",
|
||||
"visus/cuid2": "^3.0",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"guzzlehttp/psr7": "^2.6",
|
||||
"http-interop/http-factory-guzzle": "^1.2",
|
||||
"auth0/auth0-php": "^8.7",
|
||||
"auth0/auth0-php": "^8.8",
|
||||
"vlucas/phpdotenv": "^5.5",
|
||||
"leafs/form": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BitBadger\\PgSQL\\Documents\\": [ "./documents" ],
|
||||
"MyPrayerJournal\\": [ "." ],
|
||||
"MyPrayerJournal\\Domain\\": [ "./domain" ]
|
||||
}
|
||||
"visus/cuid2": "^4.0"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
621
src/app/composer.lock
generated
621
src/app/composer.lock
generated
@ -4,24 +4,25 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "5cf05fa628183ad173a48e697e23513f",
|
||||
"content-hash": "ff3f9ac8d14771409438ac21fd6919e7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "auth0/auth0-php",
|
||||
"version": "8.7.1",
|
||||
"version": "8.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/auth0/auth0-PHP.git",
|
||||
"reference": "00202f130364add3e3c5708a235ac4a8c4b239bf"
|
||||
"reference": "ef7634a50857598de44c04692bdc9b23ad201e10"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/auth0/auth0-PHP/zipball/00202f130364add3e3c5708a235ac4a8c4b239bf",
|
||||
"reference": "00202f130364add3e3c5708a235ac4a8c4b239bf",
|
||||
"url": "https://api.github.com/repos/auth0/auth0-PHP/zipball/ef7634a50857598de44c04692bdc9b23ad201e10",
|
||||
"reference": "ef7634a50857598de44c04692bdc9b23ad201e10",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-openssl": "*",
|
||||
"php": "^8.0",
|
||||
"php-http/multipart-stream-builder": "^1",
|
||||
@ -31,6 +32,7 @@
|
||||
"psr/http-message-implementation": "^1"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "^2",
|
||||
"friendsofphp/php-cs-fixer": "^3",
|
||||
"mockery/mockery": "^1",
|
||||
"pestphp/pest": "^2",
|
||||
@ -99,22 +101,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/auth0/auth0-PHP/issues",
|
||||
"source": "https://github.com/auth0/auth0-PHP/tree/8.7.1"
|
||||
"source": "https://github.com/auth0/auth0-PHP/tree/8.8.0"
|
||||
},
|
||||
"time": "2023-08-07T04:45:57+00:00"
|
||||
"time": "2023-10-18T22:54:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
"version": "3.3.2",
|
||||
"version": "3.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/semver.git",
|
||||
"reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9"
|
||||
"reference": "35e8d0af4486141bc745f23a29cc2091eb624a32"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9",
|
||||
"reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32",
|
||||
"reference": "35e8d0af4486141bc745f23a29cc2091eb624a32",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -164,9 +166,9 @@
|
||||
"versioning"
|
||||
],
|
||||
"support": {
|
||||
"irc": "irc://irc.freenode.org/composer",
|
||||
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||
"issues": "https://github.com/composer/semver/issues",
|
||||
"source": "https://github.com/composer/semver/tree/3.3.2"
|
||||
"source": "https://github.com/composer/semver/tree/3.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -182,7 +184,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-04-01T19:23:25+00:00"
|
||||
"time": "2023-08-31T09:50:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
@ -629,529 +631,6 @@
|
||||
},
|
||||
"time": "2021-07-21T13:50:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "leafs/anchor",
|
||||
"version": "v1.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/leafsphp/anchor.git",
|
||||
"reference": "debc228afd63d46d94d0c1d02629c2d912ecb4ee"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/leafsphp/anchor/zipball/debc228afd63d46d94d0c1d02629c2d912ecb4ee",
|
||||
"reference": "debc228afd63d46d94d0c1d02629c2d912ecb4ee",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest": "^1.21"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Leaf\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Darko",
|
||||
"email": "mickdd22@gmail.com",
|
||||
"homepage": "https://mychi.netlify.app",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Leaf PHP util module",
|
||||
"homepage": "https://leafphp.netlify.app/#/",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"leaf",
|
||||
"php",
|
||||
"util"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/leafsphp/anchor/issues",
|
||||
"source": "https://github.com/leafsphp/anchor/tree/v1.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/leafsphp",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/leaf",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-09T00:51:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "leafs/bareui",
|
||||
"version": "v1.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/leafsphp/bareui.git",
|
||||
"reference": "a84d855be5ba319a9c0c695d65d25aabf959b23b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/leafsphp/bareui/zipball/a84d855be5ba319a9c0c695d65d25aabf959b23b",
|
||||
"reference": "a84d855be5ba319a9c0c695d65d25aabf959b23b",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/scripts.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Leaf\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Darko",
|
||||
"email": "mickdd22@gmail.com",
|
||||
"homepage": "https://mychi.netlify.app",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Leaf PHP bareui templating engine",
|
||||
"homepage": "https://leafphp.netlify.app/#/",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"leaf",
|
||||
"php",
|
||||
"simple templating",
|
||||
"template",
|
||||
"view"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/leafsphp/bareui/issues",
|
||||
"source": "https://github.com/leafsphp/bareui/tree/v1.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/leafsphp",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/leaf",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-05-17T08:42:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "leafs/db",
|
||||
"version": "v2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/leafsphp/db.git",
|
||||
"reference": "5b663754b552dcc4dde3314e425afe75084a02f3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/leafsphp/db/zipball/5b663754b552dcc4dde3314e425afe75084a02f3",
|
||||
"reference": "5b663754b552dcc4dde3314e425afe75084a02f3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.14",
|
||||
"leafs/alchemy": "^1.0",
|
||||
"pestphp/pest": "^1.21"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Leaf\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Darko",
|
||||
"email": "mickdd22@gmail.com",
|
||||
"homepage": "https://mychi.netlify.app",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Leaf PHP db module.",
|
||||
"homepage": "https://leafphp.netlify.app/#/",
|
||||
"keywords": [
|
||||
"database",
|
||||
"framework",
|
||||
"leaf",
|
||||
"orm",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/leafsphp/db/issues",
|
||||
"source": "https://github.com/leafsphp/db/tree/v2.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/leafsphp",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/leaf",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-27T17:16:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "leafs/exception",
|
||||
"version": "v3.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/leafsphp/exceptions.git",
|
||||
"reference": "064a24f34c719a8121da2d737eddc9917ddca263"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/leafsphp/exceptions/zipball/064a24f34c719a8121da2d737eddc9917ddca263",
|
||||
"reference": "064a24f34c719a8121da2d737eddc9917ddca263",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.5.9 || ^7.0 || ^8.0",
|
||||
"psr/log": "^1.0.1 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^0.9 || ^1.0",
|
||||
"phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3",
|
||||
"symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/var-dumper": "Pretty print complex values better with var-dumper available",
|
||||
"whoops/soap": "Formats errors as SOAP responses"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Leaf\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Filipe Dobreira",
|
||||
"homepage": "https://github.com/filp",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Michael Darko",
|
||||
"email": "mickdd22@gmail.com",
|
||||
"homepage": "https://mychi.netlify.app",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Error handler for leaf (fork of whoops)",
|
||||
"homepage": "https://github.com/leafsphp/exception",
|
||||
"keywords": [
|
||||
"error",
|
||||
"exception",
|
||||
"handling",
|
||||
"library",
|
||||
"throwable",
|
||||
"whoops"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/leafsphp/exceptions/tree/v3.2.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/denis-sokolov",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-08T12:03:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "leafs/form",
|
||||
"version": "v2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/leafsphp/form.git",
|
||||
"reference": "b724596da4f52b9dc7fe1ec0cdf12b4325f369c6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/leafsphp/form/zipball/b724596da4f52b9dc7fe1ec0cdf12b4325f369c6",
|
||||
"reference": "b724596da4f52b9dc7fe1ec0cdf12b4325f369c6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"pestphp/pest": "^1.22"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Leaf\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Darko",
|
||||
"email": "mickdd22@gmail.com",
|
||||
"homepage": "https://mychi.netlify.app",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Simple straightup data validation",
|
||||
"homepage": "https://leafphp.dev/modules/forms/",
|
||||
"keywords": [
|
||||
"form",
|
||||
"framework",
|
||||
"leaf",
|
||||
"php",
|
||||
"validation"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/leafsphp/form/issues",
|
||||
"source": "https://github.com/leafsphp/form/tree/v2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/leafsphp",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/leaf",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-08-19T01:07:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "leafs/http",
|
||||
"version": "v2.2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/leafsphp/http.git",
|
||||
"reference": "77eebb3db4c722f04f9ca53ee28e9c62a5294505"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/leafsphp/http/zipball/77eebb3db4c722f04f9ca53ee28e9c62a5294505",
|
||||
"reference": "77eebb3db4c722f04f9ca53ee28e9c62a5294505",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"leafs/anchor": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Leaf\\Http\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Darko",
|
||||
"email": "mickdd22@gmail.com",
|
||||
"homepage": "https://mychi.netlify.app",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Leaf PHP HTTP module.",
|
||||
"homepage": "https://leafphp.dev/modules/http/v/2/request.html",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"headers",
|
||||
"http",
|
||||
"leaf",
|
||||
"php",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/leafsphp/http/issues",
|
||||
"source": "https://github.com/leafsphp/http/tree/v2.2.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/leafsphp",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/leaf",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-04-20T20:43:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "leafs/leaf",
|
||||
"version": "v3.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/leafsphp/leaf.git",
|
||||
"reference": "8964d19c3c129721d1baa403141499ae59c10c7f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/leafsphp/leaf/zipball/8964d19c3c129721d1baa403141499ae59c10c7f",
|
||||
"reference": "8964d19c3c129721d1baa403141499ae59c10c7f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"leafs/anchor": "*",
|
||||
"leafs/exception": "*",
|
||||
"leafs/http": "*",
|
||||
"leafs/router": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"pestphp/pest": "^1.21"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Leaf\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Darko",
|
||||
"email": "mickdd22@gmail.com",
|
||||
"homepage": "https://mychi.netlify.app",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Simple, performant and powerful PHP micro-framework for rapid web app & API development",
|
||||
"homepage": "https://leafphp.dev",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"leaf",
|
||||
"microframework",
|
||||
"php",
|
||||
"rest",
|
||||
"router"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/leafsphp/leaf/issues",
|
||||
"source": "https://github.com/leafsphp/leaf/tree/v3.4.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/leafsphp",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/leaf",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-08T12:13:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "leafs/router",
|
||||
"version": "v0.2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/leafsphp/router.git",
|
||||
"reference": "d7d66f7e76714885d878e70c7cc2c117d5c998ad"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/leafsphp/router/zipball/d7d66f7e76714885d878e70c7cc2c117d5c998ad",
|
||||
"reference": "d7d66f7e76714885d878e70c7cc2c117d5c998ad",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"leafs/anchor": "*",
|
||||
"leafs/http": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest": "^1.21"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Leaf\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Darko",
|
||||
"email": "mickdd22@gmail.com",
|
||||
"homepage": "https://mychi.netlify.app",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Leaf router module for Leaf PHP.",
|
||||
"homepage": "https://leafphp.netlify.app/#/modules/router",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"leaf",
|
||||
"php",
|
||||
"rest",
|
||||
"router"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/leafsphp/router/issues",
|
||||
"source": "https://github.com/leafsphp/router/tree/v0.2.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/leafsphp",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/leaf",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-06-30T09:40:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "netresearch/jsonmapper",
|
||||
"version": "v4.2.0",
|
||||
@ -2163,16 +1642,16 @@
|
||||
},
|
||||
{
|
||||
"name": "psr/http-client",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-client.git",
|
||||
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31"
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31",
|
||||
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31",
|
||||
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -2209,9 +1688,9 @@
|
||||
"psr-18"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-client/tree/1.0.2"
|
||||
"source": "https://github.com/php-fig/http-client"
|
||||
},
|
||||
"time": "2023-04-10T20:12:12+00:00"
|
||||
"time": "2023-09-23T14:17:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-factory",
|
||||
@ -2484,16 +1963,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
|
||||
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -2508,7 +1987,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@ -2546,7 +2025,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -2562,20 +2041,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
|
||||
"reference": "42292d99c55abe617799667f454222c54c60e229"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
|
||||
"reference": "42292d99c55abe617799667f454222c54c60e229",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -2590,7 +2069,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@ -2629,7 +2108,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -2645,20 +2124,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-07-28T09:04:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
|
||||
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -2667,7 +2146,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@ -2712,7 +2191,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -2728,20 +2207,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "visus/cuid2",
|
||||
"version": "3.0.0",
|
||||
"version": "4.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/visus-io/php-cuid2.git",
|
||||
"reference": "0a422fa4785c3ce1f01f60cec35684ed31f46860"
|
||||
"reference": "624e982aa908231f6690738d8553a3ad693b0560"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/visus-io/php-cuid2/zipball/0a422fa4785c3ce1f01f60cec35684ed31f46860",
|
||||
"reference": "0a422fa4785c3ce1f01f60cec35684ed31f46860",
|
||||
"url": "https://api.github.com/repos/visus-io/php-cuid2/zipball/624e982aa908231f6690738d8553a3ad693b0560",
|
||||
"reference": "624e982aa908231f6690738d8553a3ad693b0560",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -2770,7 +2249,7 @@
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
@ -2785,9 +2264,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/visus-io/php-cuid2/issues",
|
||||
"source": "https://github.com/visus-io/php-cuid2/tree/3.0.0"
|
||||
"source": "https://github.com/visus-io/php-cuid2/tree/4.0.0"
|
||||
},
|
||||
"time": "2023-08-11T16:22:53+00:00"
|
||||
"time": "2023-08-19T21:14:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "vlucas/phpdotenv",
|
||||
|
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BitBadger\PgSQL\Documents;
|
||||
|
||||
/**
|
||||
* Document table configuration
|
||||
*/
|
||||
class Configuration
|
||||
{
|
||||
/** @var string $connectionString The connection string to use when establishing a database connection */
|
||||
public static string $connectionString = "";
|
||||
|
||||
/** @var ?\PDO $conn The active connection */
|
||||
private static ?\PDO $conn = null;
|
||||
|
||||
/** @var ?string $startUp The name of a function to run on first connection to the database */
|
||||
public static ?string $startUp = null;
|
||||
|
||||
/**
|
||||
* Ensure that the connection string is set, either explicity, by environment variables, or with defaults
|
||||
*/
|
||||
private static function ensureConnectionString()
|
||||
{
|
||||
if (self::$connectionString == "") {
|
||||
$host = $_ENV['PGDOC_HOST'] ?? 'localhost';
|
||||
$port = $_ENV['PGDOC_PORT'] ?? 5432;
|
||||
$db = $_ENV['PGDOC_DB'] ?? 'postgres';
|
||||
$user = $_ENV['PGDOC_USER'] ?? 'postgres';
|
||||
$pass = $_ENV['PGDOC_PASS'] ?? 'postgres';
|
||||
self::$connectionString = "pgsql:host=$host;port=$port;dbname=$db;user=$user;password=$pass";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database connection, connecting on first request
|
||||
*
|
||||
* @return \PDO The PDO object representing the connection
|
||||
*/
|
||||
public static function getConn(): \PDO
|
||||
{
|
||||
if (is_null(self::$conn)) {
|
||||
self::ensureConnectionString();
|
||||
self::$conn = new \PDO(self::$connectionString);
|
||||
|
||||
if (!is_null(self::$startUp)) {
|
||||
call_user_func(self::$startUp);
|
||||
}
|
||||
}
|
||||
return self::$conn;
|
||||
}
|
||||
}
|
||||
|
||||
require('functions.php');
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
use BitBadger\PgSQL\Documents\Configuration;
|
||||
|
||||
if (!function_exists('pdo')) {
|
||||
/**
|
||||
* Return the active PostgreSQL PDO object
|
||||
*
|
||||
* @return \PDO The data connection from the configuration
|
||||
*/
|
||||
function pdo()
|
||||
{
|
||||
return Configuration::getConn();
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
(Dotenv\Dotenv::createImmutable(__DIR__))->load();
|
||||
|
||||
use MyPrayerJournal\{ AppUser, Data, Handlers };
|
||||
|
||||
Data::configure();
|
||||
|
||||
app()->template->config('path', './pages');
|
||||
app()->template->config('params', [
|
||||
'page_link' => function (string $url, bool $checkActive = false) {
|
||||
echo 'href="'. $url . '" hx-get="' . $url . '"';
|
||||
if ($checkActive && str_starts_with($_SERVER['REQUEST_URI'], $url)) {
|
||||
echo ' class="is-active-route"';
|
||||
}
|
||||
echo 'hx-target="#top" hx-swap="innerHTML" hx-push-url="true"';
|
||||
},
|
||||
'version' => 'v4',
|
||||
]);
|
||||
|
||||
app()->get('/', fn () => Handlers::render('home', 'Welcome'));
|
||||
|
||||
app()->group('/components', function () {
|
||||
app()->get('/journal-items', Handlers::journalItems(...));
|
||||
});
|
||||
app()->get('/journal', Handlers::journal(...));
|
||||
app()->group('/legal', function () {
|
||||
app()->get('/privacy-policy', fn () => Handlers::render('legal/privacy-policy', 'Privacy Policy'));
|
||||
app()->get('/terms-of-service', fn () => Handlers::render('legal/terms-of-service', 'Terms of Service'));
|
||||
});
|
||||
app()->group('/request', function () {
|
||||
app()->get( '/{reqId}/edit', Handlers::requestEdit(...));
|
||||
app()->post( '/request', Handlers::requestSave(...));
|
||||
app()->patch('/request', Handlers::requestSave(...));
|
||||
});
|
||||
app()->group('/user', function () {
|
||||
app()->get('/log-on', AppUser::logOn(...));
|
||||
app()->get('/log-on/success', AppUser::processLogOn(...));
|
||||
app()->get('/log-off', AppUser::logOff(...));
|
||||
});
|
||||
|
||||
// Extract the user's time zone from the request, if present
|
||||
app()->use(new class extends \Leaf\Middleware {
|
||||
public function call()
|
||||
{
|
||||
$_REQUEST['USER_TIME_ZONE'] = new \DateTimeZone(
|
||||
array_key_exists('HTTP_X_TIME_ZONE', $_SERVER) ? $_SERVER['HTTP_X_TIME_ZONE'] : 'Etc/UTC');
|
||||
$this->next();
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: remove before go-live
|
||||
$stdOut = fopen('php://stdout', 'w');
|
||||
function stdout(string $msg)
|
||||
{
|
||||
global $stdOut;
|
||||
fwrite($stdOut, $msg . "\n");
|
||||
}
|
||||
|
||||
app()->run();
|
67
src/app/lib/Constants.php
Normal file
67
src/app/lib/Constants.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
/**
|
||||
* Constants for use throughout the application
|
||||
*/
|
||||
class Constants
|
||||
{
|
||||
/** @var string The `$_ENV` key for the Auth0 domain configured for myPrayerJournal */
|
||||
const AUTH0_DOMAIN = 'AUTH0_DOMAIN';
|
||||
|
||||
/** @var string The `$_ENV` key for the Auth0 client ID for myPrayerJournal */
|
||||
const AUTH0_CLIENT_ID = 'AUTH0_CLIENT_ID';
|
||||
|
||||
/** @var string The `$_ENV` key for the Auth0 client secret */
|
||||
const AUTH0_CLIENT_SECRET = 'AUTH0_CLIENT_SECRET';
|
||||
|
||||
/** @var string The `$_ENV` key for the Auth0 cookie secret */
|
||||
const AUTH0_COOKIE_SECRET = 'AUTH0_COOKIE_SECRET';
|
||||
|
||||
/** @var string The `$_ENV` key for the base URL for this instance of myPrayerJournal */
|
||||
const BASE_URL = 'AUTH0_BASE_URL';
|
||||
|
||||
/** @var string The Auth0 given name (first name) claim */
|
||||
const CLAIM_GIVEN_NAME = 'given_name';
|
||||
|
||||
/** @var string The Auth0 subscriber (sub) claim */
|
||||
const CLAIM_SUB = 'sub';
|
||||
|
||||
/** @var string The name of the cookie used to persist redirection after Auth0 authentication */
|
||||
const COOKIE_REDIRECT = 'mpjredirect';
|
||||
|
||||
/** @var string the `$_SERVER` key for the HX-Request header */
|
||||
const HEADER_HX_REQUEST = 'HTTP_HX_REQUEST';
|
||||
|
||||
/** @var string The `$_SERVER` key for the HX-History-Restore-Request header */
|
||||
const HEADER_HX_HIST_REQ = 'HTTP_HX_HISTORY_RESTORE_REQUEST';
|
||||
|
||||
/** @var string The `$_SERVER` key for the X-Time-Zone header */
|
||||
const HEADER_USER_TZ = 'HTTP_X_TIME_ZONE';
|
||||
|
||||
/** @var string The `$_REQUEST` key for whether the request was initiated by htmx */
|
||||
const IS_HTMX = 'MPJ_IS_HTMX';
|
||||
|
||||
/** @var string The `$_GET` key for state passed to Auth0 if redirection is required once authenticated */
|
||||
const LOG_ON_STATE = 'state';
|
||||
|
||||
/** @var string The `$_REQUEST` key for the page title for this request */
|
||||
const PAGE_TITLE = 'MPJ_PAGE_TITLE';
|
||||
|
||||
/** @var string The `$_SERVER` key for the current page's relative URI */
|
||||
const REQUEST_URI = 'REQUEST_URI';
|
||||
|
||||
/** @var string The `$_GET` key sent to the log on page if redirection is required once authenticated */
|
||||
const RETURN_URL = 'return_url';
|
||||
|
||||
/** @var string The `$_REQUEST` key for the timezone reference to use for this request */
|
||||
const TIME_ZONE = 'MPJ_TIME_ZONE';
|
||||
|
||||
/** @var string The `$_REQUEST` key for the current user's ID */
|
||||
const USER_ID = 'MPJ_USER_ID';
|
||||
|
||||
/** @var string The `$_REQUEST` key for the current version of myPrayerJournal */
|
||||
const VERSION = 'MPJ_VERSION';
|
||||
}
|
134
src/app/lib/Data.php
Normal file
134
src/app/lib/Data.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
use BitBadger\PgDocuments\{ Definition, Document, DocumentIndex, Query };
|
||||
use MyPrayerJournal\Domain\{ History, JournalRequest, Note, Request, RequestAction };
|
||||
|
||||
class Data
|
||||
{
|
||||
/** The prayer request table */
|
||||
const REQ_TABLE = 'prayer_request';
|
||||
|
||||
/**
|
||||
* Ensure the table and index exist
|
||||
*/
|
||||
public static function startUp()
|
||||
{
|
||||
Definition::ensureTable(self::REQ_TABLE);
|
||||
Definition::ensureIndex(self::REQ_TABLE, DocumentIndex::Optimized);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Find a full prayer request by its ID
|
||||
// *
|
||||
// * @param string $reqId The request ID
|
||||
// * @param string $userId The ID of the currently logged-on user
|
||||
// * @return ?Request The request, or null if it is not found
|
||||
// */
|
||||
// public static function findFullRequestById(string $reqId, string $userId): ?Request
|
||||
// {
|
||||
// $req = Document::findById(self::REQ_TABLE, $reqId, Request::class);
|
||||
// return is_null($req) || $req->userId != $userId ? null : $req;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Add a history entry to the specified request
|
||||
// *
|
||||
// * @param string $reqId The request ID
|
||||
// * @param string $userId The ID of the currently logged-on user
|
||||
// * @param History $history The history entry to be added
|
||||
// */
|
||||
// public static function addHistory(string $reqId, string $userId, History $history)
|
||||
// {
|
||||
// $req = self::findFullRequestById($reqId, $userId);
|
||||
// if (is_null($req)) throw new \InvalidArgumentException("$reqId not found");
|
||||
// array_unshift($req->history, $history);
|
||||
// Document::updateFull(self::REQ_TABLE, $reqId, $req);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Add a note to the specified request
|
||||
// *
|
||||
// * @param string $reqId The request ID
|
||||
// * @param string $userId The ID of the currently logged-on user
|
||||
// * @param Note $note The note to be added
|
||||
// */
|
||||
// public static function addNote(string $reqId, string $userId, Note $note)
|
||||
// {
|
||||
// $req = self::findFullRequestById($reqId, $userId);
|
||||
// if (is_null($req)) throw new \InvalidArgumentException("$reqId not found");
|
||||
// array_unshift($req->notes, $note);
|
||||
// Document::updateFull(self::REQ_TABLE, $reqId, $req);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Add a new request
|
||||
// *
|
||||
// * @param Request $req The request to be added
|
||||
// */
|
||||
// public static function addRequest(Request $req)
|
||||
// {
|
||||
// Document::insert(self::REQ_TABLE, $req->id, $req);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Map an array of `Request`s to an array of `JournalRequest`s
|
||||
*
|
||||
* @param Request[] $reqs The requests to map
|
||||
* @param bool $full Whether to include history and notes (true) or not (false)
|
||||
* @return JournalRequest[] The journal request objects
|
||||
*/
|
||||
private static function mapToJournalRequest(array $reqs, bool $full): array
|
||||
{
|
||||
return array_map(fn (Request $req) => new JournalRequest($req, $full), $reqs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get journal requests for the given user by "answered" status
|
||||
*
|
||||
* @param string $userId The ID of the user for whom requests should be retrieved
|
||||
* @param string $op The JSON Path operator to use for comparison (`==` or `<>`)
|
||||
* @return JournalRequest[] The journal request objects
|
||||
*/
|
||||
private static function getJournalByAnswered(string $userId, string $op): array
|
||||
{
|
||||
$sql = Query::selectFromTable(self::REQ_TABLE)
|
||||
. ' WHERE ' . Query::whereDataContains('$1') . ' AND ' . Query::whereJsonPathMatches('$2');
|
||||
$params = [
|
||||
Query::jsonbDocParam([ 'userId' => $userId ]),
|
||||
sprintf("$.history[*].action ? (@ $op \"%s\")", RequestAction::Answered->name)
|
||||
];
|
||||
return self::mapToJournalRequest(
|
||||
Document::customList($sql, $params, Request::class, Document::mapFromJson(...)), true);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Retrieve all answered requests for this user
|
||||
// *
|
||||
// * @param string $userId The ID of the user for whom answered requests should be retrieved
|
||||
// * @return JournalRequest[] The answered requests
|
||||
// */
|
||||
// public static function getAnsweredRequests(string $userId): array
|
||||
// {
|
||||
// $answered = self::getJournalByAnswered($userId, '==');
|
||||
// usort($answered,
|
||||
// fn (JournalRequest $a, JournalRequest $b) => $a->asOf == $b->asOf ? 0 : ($a->asOf > $b->asOf ? -1 : 1));
|
||||
// return $answered;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Get the user's current prayer request journal
|
||||
*
|
||||
* @param string $userId The ID of the user whose journal should be retrieved
|
||||
* @return JournalRequest[] The journal request objects
|
||||
*/
|
||||
public static function getJournal(string $userId): array
|
||||
{
|
||||
$reqs = self::getJournalByAnswered($userId, '<>');
|
||||
usort($reqs,
|
||||
fn (JournalRequest $a, JournalRequest $b) => $a->asOf == $b->asOf ? 0 : ($a->asOf < $b->asOf ? -1 : 1));
|
||||
return $reqs;
|
||||
}
|
||||
}
|
63
src/app/lib/Dates.php
Normal file
63
src/app/lib/Dates.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
use DateTimeImmutable, DateTimeInterface, DateTimeZone;
|
||||
|
||||
class Dates
|
||||
{
|
||||
/** Minutes in a day */
|
||||
private const A_DAY = 1_440;
|
||||
|
||||
/** Minutes in two days(-ish) */
|
||||
private const ALMOST_2_DAYS = 2_520;
|
||||
|
||||
/** Minutes in a month */
|
||||
private const A_MONTH = 43_200;
|
||||
|
||||
/** Minutes in two months */
|
||||
private const TWO_MONTHS = 86_400;
|
||||
|
||||
/**
|
||||
* Get a UTC-referenced current date/time
|
||||
*
|
||||
* @return DateTimeImmutable The current date/time with UTC reference
|
||||
*/
|
||||
public static function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable(timezone: new DateTimeZone('Etc/UTC'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the distance between two instants in approximate English terms
|
||||
*
|
||||
* @param DateTimeInterface $startOn The starting date/time for the comparison
|
||||
* @param DateTimeInterface $endOn THe ending date/time for the comparison
|
||||
* @return string The formatted interval
|
||||
*/
|
||||
public static function formatDistance(DateTimeInterface $startOn, DateTimeInterface $endOn): string
|
||||
{
|
||||
$diff = $startOn->diff($endOn);
|
||||
$minutes =
|
||||
$diff->i + ($diff->h * 60) + ($diff->d * 60 * 24) + ($diff->m * 60 * 24 * 30) + ($diff->y * 60 * 24 * 365);
|
||||
$months = round($minutes / self::A_MONTH);
|
||||
$years = $months / 12;
|
||||
[ $format, $number ] = match (true) {
|
||||
$minutes < 1 => [ DistanceFormat::LessThanXMinutes, 1 ],
|
||||
$minutes < 45 => [ DistanceFormat::XMinutes, $minutes ],
|
||||
$minutes < 90 => [ DistanceFormat::AboutXHours, 1 ],
|
||||
$minutes < self::A_DAY => [ DistanceFormat::AboutXHours, round($minutes / 60) ],
|
||||
$minutes < self::ALMOST_2_DAYS => [ DistanceFormat::XDays, 1 ],
|
||||
$minutes < self::A_MONTH => [ DistanceFormat::XDays, round($minutes / self::A_DAY) ],
|
||||
$minutes < self::TWO_MONTHS => [ DistanceFormat::AboutXMonths, round($minutes / self::A_MONTH) ],
|
||||
$months < 12 => [ DistanceFormat::XMonths, round($minutes / self::A_MONTH) ],
|
||||
$months % 12 < 3 => [ DistanceFormat::AboutXYears, $years ],
|
||||
$months % 12 < 9 => [ DistanceFormat::OverXYears, $years ],
|
||||
default => [ DistanceFormat::AlmostXYears, $years + 1 ],
|
||||
};
|
||||
|
||||
$relativeWords = sprintf(DistanceFormat::format($format, $number == 1), $number);
|
||||
return $startOn > $endOn ? "$relativeWords ago" : "in $relativeWords";
|
||||
}
|
||||
}
|
50
src/app/lib/DistanceFormat.php
Normal file
50
src/app/lib/DistanceFormat.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
/**
|
||||
* The different distance formats supported
|
||||
*/
|
||||
enum DistanceFormat
|
||||
{
|
||||
case LessThanXMinutes;
|
||||
case XMinutes;
|
||||
case AboutXHours;
|
||||
case XHours;
|
||||
case XDays;
|
||||
case AboutXWeeks;
|
||||
case XWeeks;
|
||||
case AboutXMonths;
|
||||
case XMonths;
|
||||
case AboutXYears;
|
||||
case XYears;
|
||||
case OverXYears;
|
||||
case AlmostXYears;
|
||||
|
||||
/**
|
||||
* Return the formatting string for the given format and number
|
||||
*
|
||||
* @param DistanceFormat $it The distance format
|
||||
* @param bool $singular If true, returns the singular version; if false (default), returns the plural version
|
||||
* @return string The format string
|
||||
*/
|
||||
public static function format(DistanceFormat $it, bool $singular = false): string
|
||||
{
|
||||
return match ($it) {
|
||||
self::LessThanXMinutes => $singular ? 'less than a minute' : 'less than %i minutes',
|
||||
self::XMinutes => $singular ? 'a minute' : '%i minutes',
|
||||
self::AboutXHours => $singular ? 'about an hour' : 'about %i hours',
|
||||
self::XHours => $singular ? 'an hour' : '%i hours',
|
||||
self::XDays => $singular ? 'a day' : '%i days',
|
||||
self::AboutXWeeks => $singular ? 'about a week' : 'about %i weeks',
|
||||
self::XWeeks => $singular ? 'a week' : '%i weeks',
|
||||
self::AboutXMonths => $singular ? 'about a month' : 'about %i months',
|
||||
self::XMonths => $singular ? 'a month' : '%i months',
|
||||
self::AboutXYears => $singular ? 'about a year' : 'about %i years',
|
||||
self::XYears => $singular ? 'a year' : '%i years',
|
||||
self::OverXYears => $singular ? 'over a year' : 'over %i years',
|
||||
self::AlmostXYears => $singular ? 'almost a year' : 'almost %i years',
|
||||
};
|
||||
}
|
||||
}
|
70
src/app/lib/documents/Configuration.php
Normal file
70
src/app/lib/documents/Configuration.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BitBadger\PgDocuments;
|
||||
|
||||
use PgSql\Connection;
|
||||
|
||||
/**
|
||||
* Document table configuration
|
||||
*/
|
||||
class Configuration
|
||||
{
|
||||
/** @var string $connectionString The connection string to use when establishing a database connection */
|
||||
public static string $connectionString = "";
|
||||
|
||||
/** @var Connection $pgConn The active connection */
|
||||
private static ?Connection $pgConn = null;
|
||||
|
||||
/** @var ?string $startUp The name of a function to run on first connection to the database */
|
||||
public static ?string $startUp = null;
|
||||
|
||||
/**
|
||||
* Ensure that the connection string is set, either explicity, by environment variables, or with defaults
|
||||
*/
|
||||
private static function ensureConnectionString()
|
||||
{
|
||||
if (self::$connectionString == "") {
|
||||
$host = $_ENV['PGDOC_HOST'] ?? 'localhost';
|
||||
$port = $_ENV['PGDOC_PORT'] ?? 5432;
|
||||
$db = $_ENV['PGDOC_DB'] ?? 'postgres';
|
||||
$user = $_ENV['PGDOC_USER'] ?? 'postgres';
|
||||
$pass = $_ENV['PGDOC_PASS'] ?? 'postgres';
|
||||
$opts = $_ENV['PGDOC_OPTS'] ?? '';
|
||||
self::$connectionString = "host=$host port=$port dbname=$db user=$user password=$pass"
|
||||
. ($opts ? " options='$opts'" : '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A no-op function to force this file to be autoloaded if no explicit configuration is required
|
||||
*/
|
||||
public static function init() { }
|
||||
|
||||
/**
|
||||
* Get the PostgreSQL connection, connecting on first request
|
||||
*
|
||||
* @return Connection The open PostgreSQL connection
|
||||
*/
|
||||
public static function getPgConn(): Connection
|
||||
{
|
||||
if (is_null(self::$pgConn)) {
|
||||
self::ensureConnectionString();
|
||||
self::$pgConn = pg_connect(self::$connectionString);
|
||||
}
|
||||
return self::$pgConn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the PostgreSQL connection if it is open
|
||||
*/
|
||||
public static function closeConn()
|
||||
{
|
||||
if (!is_null(self::$pgConn)) {
|
||||
pg_close(self::$pgConn);
|
||||
self::$pgConn = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require('functions.php');
|
@ -1,7 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BitBadger\PgSQL\Documents;
|
||||
namespace BitBadger\PgDocuments;
|
||||
|
||||
use PgSql\Result;
|
||||
|
||||
/**
|
||||
* Methods to define tables and indexes for document tables
|
||||
@ -41,7 +43,9 @@ class Definition
|
||||
*/
|
||||
public static function ensureTable(string $name)
|
||||
{
|
||||
pdo()->query(self::createTable($name))->execute();
|
||||
/** @var Result|bool */
|
||||
$result = pg_query(pg_conn(), self::createTable($name));
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,6 +56,8 @@ class Definition
|
||||
*/
|
||||
public static function ensureIndex(string $name, DocumentIndex $type)
|
||||
{
|
||||
pdo()->query(self::createIndex($name, $type))->execute();
|
||||
/** @var Result|bool */
|
||||
$result = pg_query(pg_conn(), self::createIndex($name, $type));
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
}
|
@ -1,18 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BitBadger\PgSQL\Documents;
|
||||
namespace BitBadger\PgDocuments;
|
||||
|
||||
use PDOStatement;
|
||||
use JsonMapper;
|
||||
use PgSql\Result;
|
||||
|
||||
/** Document manipulation functions */
|
||||
class Document
|
||||
{
|
||||
/** JSON Mapper instance to use for creating a domain type instance from a document */
|
||||
private static ?\JsonMapper $mapper = null;
|
||||
|
||||
/** Attribute that prevents PDO from attempting its own PREPARE on a query */
|
||||
private const NO_PREPARE = [ \PDO::ATTR_EMULATE_PREPARES => false ];
|
||||
private static ?JsonMapper $mapper = null;
|
||||
|
||||
/**
|
||||
* Map a domain type from the JSON document retrieved
|
||||
@ -25,7 +23,7 @@ class Document
|
||||
public static function mapDocFromJson(string $columnName, array $result, string $className): mixed
|
||||
{
|
||||
if (is_null(self::$mapper)) {
|
||||
self::$mapper = new \JsonMapper();
|
||||
self::$mapper = new JsonMapper();
|
||||
}
|
||||
|
||||
$mapped = new $className();
|
||||
@ -54,10 +52,9 @@ class Document
|
||||
*/
|
||||
private static function executeNonQuery(string $query, string $docId, array|object $document)
|
||||
{
|
||||
$nonQuery = pdo()->prepare($query, self::NO_PREPARE);
|
||||
$nonQuery->bindParam(':id', $docId);
|
||||
$nonQuery->bindParam(':data', Query::jsonbDocParam($document));
|
||||
$nonQuery->execute();
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), $query, [ $docId, Query::jsonbDocParam($document) ]);
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,6 +81,23 @@ class Document
|
||||
self::executeNonQuery(Query::save($tableName), $docId, $document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a count query, returning the `it` parameter of that query as an integer
|
||||
*
|
||||
* @param string $sql The SQL query that will return a count
|
||||
* @param array $params Parameters needed for that query
|
||||
* @return int The count of matching rows for the query
|
||||
*/
|
||||
private static function runCount(string $sql, array $params): int
|
||||
{
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), $sql, $params);
|
||||
if (!$result) return -1;
|
||||
$count = intval(pg_fetch_assoc($result)['it']);
|
||||
pg_free_result($result);
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all documents in a table
|
||||
*
|
||||
@ -92,8 +106,7 @@ class Document
|
||||
*/
|
||||
public static function countAll(string $tableName): int
|
||||
{
|
||||
$result = pdo()->query(Query::countAll($tableName))->fetch(\PDO::FETCH_ASSOC);
|
||||
return intval($result['it']);
|
||||
return self::runCount(Query::countAll($tableName), []);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,11 +118,7 @@ class Document
|
||||
*/
|
||||
public static function countByContains(string $tableName, array|object $criteria): int
|
||||
{
|
||||
$query = pdo()->prepare(Query::countByContains($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':criteria', Query::jsonbDocParam($criteria));
|
||||
$query->execute();
|
||||
$result = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
return intval($result['it']);
|
||||
return self::runCount(Query::countByContains($tableName), [ Query::jsonbDocParam($criteria) ]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,11 +130,24 @@ class Document
|
||||
*/
|
||||
public static function countByJsonPath(string $tableName, string $jsonPath): int
|
||||
{
|
||||
$query = pdo()->prepare(Query::countByContains($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':path', $jsonPath);
|
||||
$query->execute();
|
||||
$result = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
return intval($result['it']);
|
||||
return self::runCount(Query::countByJsonPath($tableName), [ $jsonPath ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an existence query (returning the `it` parameter of that query)
|
||||
*
|
||||
* @param string $sql The SQL query that will return existence
|
||||
* @param array $params Parameters needed for that query
|
||||
* @return bool The result of the existence query
|
||||
*/
|
||||
private static function runExists(string $sql, array $params): bool
|
||||
{
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), $sql, $params);
|
||||
if (!$result) return -1;
|
||||
$exists = boolval(pg_fetch_assoc($result)['it']);
|
||||
pg_free_result($result);
|
||||
return $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -137,11 +159,7 @@ class Document
|
||||
*/
|
||||
public static function existsById(string $tableName, string $docId): bool
|
||||
{
|
||||
$query = pdo()->prepare(Query::existsById($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':id', $docId);
|
||||
$query->execute();
|
||||
$result = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
return boolval($result['it']);
|
||||
return self::runExists(Query::existsById($tableName), [ $docId ]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,11 +171,7 @@ class Document
|
||||
*/
|
||||
public static function existsByContains(string $tableName, array|object $criteria): bool
|
||||
{
|
||||
$query = pdo()->prepare(Query::existsByContains($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':criteria', Query::jsonbDocParam($criteria));
|
||||
$query->execute();
|
||||
$result = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
return boolval($result['it']);
|
||||
return self::runExists(Query::existsByContains($tableName), [ Query::jsonbDocParam($criteria) ]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -169,23 +183,27 @@ class Document
|
||||
*/
|
||||
public static function existsByJsonPath(string $tableName, string $jsonPath): bool
|
||||
{
|
||||
$query = pdo()->prepare(Query::existsByJsonPath($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':path', $jsonPath);
|
||||
$query->execute();
|
||||
$result = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
return boolval($result['it']);
|
||||
return self::runExists(Query::existsByJsonPath($tableName), [ $jsonPath ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the results of a query to domain type objects
|
||||
* Run a query, mapping the results to an array of domain type objects
|
||||
*
|
||||
* @param \PDOStatement $stmt The statement with the query to be run
|
||||
* @param string $sql The query to be run
|
||||
* @param array $params The parameters for the query
|
||||
* @param class-string<Type> $className The type of document to be mapped
|
||||
* @return array<Type> The documents matching the query
|
||||
*/
|
||||
private static function mapResults(\PDOStatement $stmt, string $className): array
|
||||
private static function runListQuery(string $sql, array $params, string $className): array
|
||||
{
|
||||
return array_map(fn ($it) => self::mapFromJson($it, $className), $stmt->fetchAll(\PDO::FETCH_ASSOC));
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), $sql, $params);
|
||||
try {
|
||||
if (!$result || pg_result_status($result) == PGSQL_EMPTY_QUERY) return [];
|
||||
return array_map(fn ($it) => self::mapFromJson($it, $className), pg_fetch_all($result));
|
||||
} finally {
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -197,7 +215,7 @@ class Document
|
||||
*/
|
||||
public static function findAll(string $tableName, string $className): array
|
||||
{
|
||||
return self::mapResults(pdo()->query(Query::selectFromTable($tableName)), $className);
|
||||
return self::runListQuery(Query::selectFromTable($tableName), [], $className);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -210,26 +228,8 @@ class Document
|
||||
*/
|
||||
public static function findById(string $tableName, string $docId, string $className): mixed
|
||||
{
|
||||
$query = pdo()->prepare(Query::findById($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':id', $docId);
|
||||
$query->execute();
|
||||
$result = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
return $result ? self::mapFromJson($result, $className) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSON containment query
|
||||
*
|
||||
* @param string $tableName The table from which documents should be retrieved
|
||||
* @param array|object $criteria The criteria for the JSON containment query
|
||||
* @return \PDOStatement An executed query ready to be fetched
|
||||
*/
|
||||
private static function queryByContains(string $tableName, array|object $criteria): \PDOStatement
|
||||
{
|
||||
$query = pdo()->prepare(Query::findByContains($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':criteria', Query::jsonbDocParam($criteria));
|
||||
$query->execute();
|
||||
return $query;
|
||||
$results = self::runListQuery(Query::findById($tableName), [ $docId ], $className);
|
||||
return $results ? $results[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -242,7 +242,7 @@ class Document
|
||||
*/
|
||||
public static function findByContains(string $tableName, array|object $criteria, string $className): array
|
||||
{
|
||||
return self::mapResults(self::queryByContains($tableName, $criteria), $className);
|
||||
return self::runListQuery(Query::findByContains($tableName), [ Query::jsonbDocParam($criteria) ], $className);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -255,24 +255,9 @@ class Document
|
||||
*/
|
||||
public static function findFirstByContains(string $tableName, array|object $criteria, string $className): mixed
|
||||
{
|
||||
$query = self::queryByContains($tableName, $criteria);
|
||||
$result = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
return $result ? self::mapFromJson($result, $className) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve documents in a table via JSON Path match `@?`
|
||||
*
|
||||
* @param string $tableName The table from which documents should be retrieved
|
||||
* @param string $jsonPath The JSON Path to be matched
|
||||
* @return \PDOStatement An executed query ready to be fetched
|
||||
*/
|
||||
private static function queryByJsonPath(string $tableName, string $jsonPath): \PDOStatement
|
||||
{
|
||||
$query = pdo()->prepare(Query::findByJsonPath($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':path', $jsonPath);
|
||||
$query->execute();
|
||||
return $query;
|
||||
$results = self::runListQuery(Query::findByContains($tableName) . ' LIMIT 1',
|
||||
[ Query::jsonbDocParam($criteria) ], $className);
|
||||
return $results ? $results[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -285,7 +270,7 @@ class Document
|
||||
*/
|
||||
public static function findByJsonPath(string $tableName, string $jsonPath, string $className): array
|
||||
{
|
||||
return self::mapResults(self::queryByJsonPath($tableName, $jsonPath), $className);
|
||||
return self::runListQuery(Query::findByJsonPath($tableName), [ $jsonPath ], $className);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -298,9 +283,8 @@ class Document
|
||||
*/
|
||||
public static function findFirstByJsonPath(string $tableName, string $jsonPath, string $className): mixed
|
||||
{
|
||||
$query = self::queryByJsonPath($tableName, $jsonPath);
|
||||
$result = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
return $result ? self::mapFromJson($result, $className) : null;
|
||||
$results = self::runListQuery(Query::findByJsonPath($tableName) . ' LIMIT 1', [ $jsonPath ], $className);
|
||||
return $results ? $results[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -336,10 +320,10 @@ class Document
|
||||
*/
|
||||
public static function updatePartialByContains(string $tableName, array|object $criteria, array|object $document)
|
||||
{
|
||||
$query = pdo()->prepare(Query::updatePartialByContains($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':data', Query::jsonbDocParam($document));
|
||||
$query->bindParam(':criteria', Query::jsonbDocParam($criteria));
|
||||
$query->execute();
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), Query::updatePartialByContains($tableName),
|
||||
[ Query::jsonbDocParam($criteria), Query::jsonbDocParam($document) ]);
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -351,10 +335,10 @@ class Document
|
||||
*/
|
||||
public static function updatePartialByJsonPath(string $tableName, string $jsonPath, array|object $document)
|
||||
{
|
||||
$query = pdo()->prepare(Query::updatePartialByContains($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':data', Query::jsonbDocParam($document));
|
||||
$query->bindParam(':path', $jsonPath);
|
||||
$query->execute();
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), Query::updatePartialByContains($tableName),
|
||||
[ $jsonPath, Query::jsonbDocParam($document) ]);
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -376,9 +360,9 @@ class Document
|
||||
*/
|
||||
public static function deleteByContains(string $tableName, array|object $criteria)
|
||||
{
|
||||
$query = pdo()->prepare(Query::deleteByContains($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':criteria', Query::jsonbDocParam($criteria));
|
||||
$query->execute();
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), Query::deleteByContains($tableName), [ Query::jsonbDocParam($criteria) ]);
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -389,67 +373,57 @@ class Document
|
||||
*/
|
||||
public static function deleteByJsonPath(string $tableName, string $jsonPath)
|
||||
{
|
||||
$query = pdo()->prepare(Query::deleteByJsonPath($tableName), self::NO_PREPARE);
|
||||
$query->bindParam(':path', $jsonPath);
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
// TODO: custom
|
||||
|
||||
/**
|
||||
* Create and execute a custom query
|
||||
*
|
||||
* @param string $sql The SQL query to execute
|
||||
* @param array $params An associative array of parameters for the SQL query
|
||||
* @return PDOStatement The query, executed and ready to be fetched
|
||||
*/
|
||||
private static function createCustomQuery(string $sql, array $params): PDOStatement
|
||||
{
|
||||
$query = pdo()->prepare($sql, [ \PDO::ATTR_EMULATE_PREPARES => false ]);
|
||||
array_walk($params, fn ($value, $name) => $query->bindParam($name, $value));
|
||||
$query->execute();
|
||||
return $query;
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), Query::deleteByJsonPath($tableName), [ $jsonPath ]);
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve documents via a custom query and mapping
|
||||
*
|
||||
* @param string $sql The SQL query to execute
|
||||
* @param array $params An associative array of parameters for the SQL query
|
||||
* @param array $params A positional array of parameters for the SQL query
|
||||
* @param callable $mapFunc A function that expects an associative array and returns a value of the desired type
|
||||
* @param class-string<Type> $className The type of document to be mapped
|
||||
* @return array<Type> The documents matching the query
|
||||
*/
|
||||
public static function customList(string $sql, array $params, string $className, callable $mapFunc): array
|
||||
{
|
||||
return array_map(
|
||||
fn ($it) => $mapFunc($it, $className),
|
||||
Document::createCustomQuery($sql, $params)->fetchAll(\PDO::FETCH_ASSOC));
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), $sql, $params);
|
||||
try {
|
||||
if (!$result || pg_result_status($result) == PGSQL_EMPTY_QUERY) return [];
|
||||
return array_map(fn ($it) => $mapFunc($it, $className), pg_fetch_all($result));
|
||||
} finally {
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a document via a custom query and mapping
|
||||
*
|
||||
* @param string $sql The SQL query to execute
|
||||
* @param array $params An associative array of parameters for the SQL query
|
||||
* @param string $sql The SQL query to execute ("LIMIT 1" will be appended)
|
||||
* @param array $params A positional array of parameters for the SQL query
|
||||
* @param callable $mapFunc A function that expects an associative array and returns a value of the desired type
|
||||
* @param class-string<Type> $className The type of document to be mapped
|
||||
* @return ?Type The document matching the query, or null if none is found
|
||||
*/
|
||||
public static function customSingle(string $sql, array $params, string $className, callable $mapFunc): mixed
|
||||
{
|
||||
$result = self::createCustomQuery($sql, $params)->fetch(\PDO::FETCH_ASSOC);
|
||||
return $result ? $mapFunc($result, $className) : null;
|
||||
$results = self::customList("$sql LIMIT 1", $params, $className, $mapFunc);
|
||||
return $results ? $results[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a custom query that does not return a result
|
||||
*
|
||||
* @param string $sql The SQL query to execute
|
||||
* @param array $params An associative array of parameters for the SQL query
|
||||
* @param array $params A positional array of parameters for the SQL query
|
||||
*/
|
||||
public static function customNonQuery(string $sql, array $params)
|
||||
{
|
||||
self::createCustomQuery($sql, $params);
|
||||
/** @var Result|bool */
|
||||
$result = pg_query_params(pg_conn(), $sql, $params);
|
||||
if ($result) pg_free_result($result);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BitBadger\PgSQL\Documents;
|
||||
namespace BitBadger\PgDocuments;
|
||||
|
||||
/** The type of index to generate for the document */
|
||||
enum DocumentIndex
|
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BitBadger\PgSQL\Documents;
|
||||
namespace BitBadger\PgDocuments;
|
||||
|
||||
/** Query construction functions */
|
||||
class Query
|
||||
@ -25,7 +25,7 @@ class Query
|
||||
*/
|
||||
public static function whereDataContains(string $paramName): string
|
||||
{
|
||||
return "data @> $paramName::jsonb";
|
||||
return "data @> $paramName";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -36,7 +36,7 @@ class Query
|
||||
*/
|
||||
public static function whereJsonPathMatches(string $paramName): string
|
||||
{
|
||||
return "data @?? {$paramName}::jsonpath";
|
||||
return "data @? $paramName::jsonpath";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,7 +62,7 @@ class Query
|
||||
*/
|
||||
public static function insert(string $tableName): string
|
||||
{
|
||||
return "INSERT INTO $tableName (id, data) VALUES (:id, :data)";
|
||||
return "INSERT INTO $tableName (id, data) VALUES ($1, $2)";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,7 +73,7 @@ class Query
|
||||
*/
|
||||
public static function save(string $tableName): string
|
||||
{
|
||||
return "INSERT INTO $tableName (id, data) VALUES (:id, :data)
|
||||
return "INSERT INTO $tableName (id, data) VALUES ($1, $2)
|
||||
ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data";
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ class Query
|
||||
*/
|
||||
public static function countAll(string $tableName): string
|
||||
{
|
||||
return "SELECT COUNT(*) AS it FROM $tableName";
|
||||
return "SELECT COUNT(id) AS it FROM $tableName";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,7 +96,7 @@ class Query
|
||||
*/
|
||||
public static function countByContains(string $tableName): string
|
||||
{
|
||||
return sprintf("SELECT COUNT(*) AS it FROM $tableName WHERE %s", self::whereDataContains(':criteria'));
|
||||
return "SELECT COUNT(id) AS it FROM $tableName WHERE " . self::whereDataContains('$1');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -107,7 +107,7 @@ class Query
|
||||
*/
|
||||
public static function countByJsonPath(string $tableName): string
|
||||
{
|
||||
return sprintf("SELECT COUNT(*) AS it FROM $tableName WHERE %s", self::whereJsonPathMatches(':path'));
|
||||
return "SELECT COUNT(id) AS it FROM $tableName WHERE " . self::whereJsonPathMatches('$1');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -118,7 +118,7 @@ class Query
|
||||
*/
|
||||
public static function existsById(string $tableName): string
|
||||
{
|
||||
return "SELECT EXISTS (SELECT 1 FROM $tableName WHERE id = :id) AS it";
|
||||
return "SELECT EXISTS (SELECT 1 FROM $tableName WHERE id = $1) AS it";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,7 +129,7 @@ class Query
|
||||
*/
|
||||
public static function existsByContains(string $tableName): string
|
||||
{
|
||||
return sprintf("SELECT EXISTS (SELECT 1 FROM $tableName WHERE %s AS it", self::whereDataContains(':criteria'));
|
||||
return "SELECT EXISTS (SELECT 1 FROM $tableName WHERE " . self::whereDataContains('$1') . ' AS it';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,7 +140,7 @@ class Query
|
||||
*/
|
||||
public static function existsByJsonPath(string $tableName): string
|
||||
{
|
||||
return sprintf("SELECT EXISTS (SELECT 1 FROM $tableName WHERE %s AS it", self::whereJsonPathMatches(':path'));
|
||||
return "SELECT EXISTS (SELECT 1 FROM $tableName WHERE " . self::whereJsonPathMatches('$1') . ' AS it';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -151,7 +151,7 @@ class Query
|
||||
*/
|
||||
public static function findById(string $tableName): string
|
||||
{
|
||||
return sprintf('%s WHERE id = :id', self::selectFromTable($tableName));
|
||||
return self::selectFromTable($tableName) . ' WHERE id = $1';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -162,7 +162,7 @@ class Query
|
||||
*/
|
||||
public static function findByContains(string $tableName): string
|
||||
{
|
||||
return sprintf('%s WHERE %s', self::selectFromTable($tableName), self::whereDataContains(':criteria'));
|
||||
return self::selectFromTable($tableName) . ' WHERE ' . self::whereDataContains('$1');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,7 +173,7 @@ class Query
|
||||
*/
|
||||
public static function findByJsonPath(string $tableName): string
|
||||
{
|
||||
return sprintf('%s WHERE %s', self::selectFromTable($tableName), self::whereJsonPathMatches(':path'));
|
||||
return self::selectFromTable($tableName) . ' WHERE ' . self::whereJsonPathMatches('$1');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,7 +184,7 @@ class Query
|
||||
*/
|
||||
public static function updateFull(string $tableName): string
|
||||
{
|
||||
return "UPDATE $tableName SET data = :data WHERE id = :id";
|
||||
return "UPDATE $tableName SET data = $2 WHERE id = $1";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -195,7 +195,7 @@ class Query
|
||||
*/
|
||||
public static function updatePartialById(string $tableName): string
|
||||
{
|
||||
return "UPDATE $tableName SET data = data || :data WHERE id = :id";
|
||||
return "UPDATE $tableName SET data = data || $2 WHERE id = $1";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -206,7 +206,7 @@ class Query
|
||||
*/
|
||||
public static function updatePartialByContains(string $tableName): string
|
||||
{
|
||||
return sprintf("UPDATE $tableName SET data = data || :data WHERE %s", self::whereDataContains(':criteria'));
|
||||
return "UPDATE $tableName SET data = data || $2 WHERE " . self::whereDataContains('$1');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -217,7 +217,7 @@ class Query
|
||||
*/
|
||||
public static function updatePartialByJsonPath(string $tableName): string
|
||||
{
|
||||
return sprintf("UPDATE $tableName SET data = data || :data WHERE %s", self::whereJsonPathMatches(':path'));
|
||||
return "UPDATE $tableName SET data = data || $2 WHERE " . self::whereJsonPathMatches('$1');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -228,7 +228,7 @@ class Query
|
||||
*/
|
||||
public static function deleteById(string $tableName): string
|
||||
{
|
||||
return "DELETE FROM $tableName WHERE id = :id";
|
||||
return "DELETE FROM $tableName WHERE id = $1";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -239,7 +239,7 @@ class Query
|
||||
*/
|
||||
public static function deleteByContains(string $tableName): string
|
||||
{
|
||||
return sprintf("DELETE FROM $tableName WHERE %s", self::whereDataContains(':criteria'));
|
||||
return "DELETE FROM $tableName WHERE " . self::whereDataContains('$1');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -250,6 +250,6 @@ class Query
|
||||
*/
|
||||
public static function deleteByJsonPath(string $tableName): string
|
||||
{
|
||||
return sprintf("DELETE FROM $tableName WHERE %s", self::whereJsonPathMatches(':path'));
|
||||
return "DELETE FROM $tableName WHERE " . self::whereJsonPathMatches('$1');
|
||||
}
|
||||
}
|
16
src/app/lib/documents/functions.php
Normal file
16
src/app/lib/documents/functions.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use BitBadger\PgDocuments\Configuration;
|
||||
use PgSql\Connection;
|
||||
|
||||
if (!function_exists('pg_conn')) {
|
||||
/**
|
||||
* Return the active PostgreSQL connection
|
||||
*
|
||||
* @return Connection The data connection from the configuration
|
||||
*/
|
||||
function pg_conn(): Connection
|
||||
{
|
||||
return Configuration::getPgConn();
|
||||
}
|
||||
}
|
@ -3,10 +3,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
trait AsOf
|
||||
{
|
||||
/** The "as of" date/time */
|
||||
public \DateTimeImmutable $asOf;
|
||||
public DateTimeImmutable $asOf;
|
||||
|
||||
/**
|
||||
* Sort an as-of item from oldest to newest
|
@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
use DateTimeImmutable, DateTimeZone;
|
||||
|
||||
/**
|
||||
* A record of action taken on a prayer request, including updates to its text
|
||||
*/
|
||||
@ -18,7 +20,7 @@ class History
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->asOf = new \DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
|
||||
$this->asOf = new DateTimeImmutable('1/1/1970', new DateTimeZone('Etc/UTC'));
|
||||
}
|
||||
|
||||
public function isCreated(): bool
|
@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
use DateTimeImmutable, DateTimeZone;
|
||||
|
||||
/**
|
||||
* A prayer request, along with calculated fields, for use in displaying journal lists
|
||||
*/
|
||||
@ -20,16 +22,16 @@ class JournalRequest
|
||||
public string $text = '';
|
||||
|
||||
/** The date/time this request was last marked as prayed */
|
||||
public \DateTimeImmutable $lastPrayed;
|
||||
public DateTimeImmutable $lastPrayed;
|
||||
|
||||
/** The last action taken on this request */
|
||||
public RequestAction $lastAction = RequestAction::Created;
|
||||
|
||||
/** When this request will be shown again after having been snoozed */
|
||||
public ?\DateTimeImmutable $snoozedUntil = null;
|
||||
public ?DateTimeImmutable $snoozedUntil = null;
|
||||
|
||||
/** When this request will be show agains after a non-immediate recurrence */
|
||||
public ?\DateTimeImmutable $showAfter = null;
|
||||
public ?DateTimeImmutable $showAfter = null;
|
||||
|
||||
/** The type of recurrence for this request */
|
||||
public RecurrenceType $recurrenceType = RecurrenceType::Immediate;
|
||||
@ -58,8 +60,8 @@ class JournalRequest
|
||||
public function __construct(?Request $req = null, bool $full = false)
|
||||
{
|
||||
if (is_null($req)) {
|
||||
$this->asOf = new \DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
|
||||
$this->lastPrayed = new \DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
|
||||
$this->asOf = new DateTimeImmutable('1/1/1970', new DateTimeZone('Etc/UTC'));
|
||||
$this->lastPrayed = new DateTimeImmutable('1/1/1970', new DateTimeZone('Etc/UTC'));
|
||||
} else {
|
||||
$this->id = $req->id;
|
||||
$this->userId = $req->userId;
|
@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
use DateTimeImmutable, DateTimeZone;
|
||||
|
||||
/**
|
||||
* A note entered on a prayer request
|
||||
*/
|
||||
@ -15,6 +17,6 @@ class Note
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->asOf = new \DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
|
||||
$this->asOf = new DateTimeImmutable('1/1/1970', new DateTimeZone('Etc/UTC'));
|
||||
}
|
||||
}
|
@ -3,10 +3,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* The unit to use when determining when to show a recurring request
|
||||
*/
|
||||
enum RecurrenceType implements \JsonSerializable
|
||||
enum RecurrenceType implements JsonSerializable
|
||||
{
|
||||
/** The request should reappear immediately */
|
||||
case Immediate;
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
use DateTimeImmutable, DateTimeZone;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
/**
|
||||
@ -14,16 +15,16 @@ class Request
|
||||
public string $id;
|
||||
|
||||
/** The date/time the request was originally entered */
|
||||
public \DateTimeImmutable $enteredOn;
|
||||
public DateTimeImmutable $enteredOn;
|
||||
|
||||
/** The ID of the user to whom this request belongs */
|
||||
public string $userId = '';
|
||||
|
||||
/** The date/time the snooze expires for this request */
|
||||
public ?\DateTimeImmutable $snoozedUntil = null;
|
||||
public ?DateTimeImmutable $snoozedUntil = null;
|
||||
|
||||
/** The date/time this request should once again show as defined by recurrence */
|
||||
public ?\DateTimeImmutable $showAfter = null;
|
||||
public ?DateTimeImmutable $showAfter = null;
|
||||
|
||||
/** The type of recurrence for this request */
|
||||
public RecurrenceType $recurrenceType = RecurrenceType::Immediate;
|
||||
@ -46,6 +47,6 @@ class Request
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = new Cuid2();
|
||||
$this->enteredOn = new \DateTimeImmutable('1/1/1970', new \DateTimeZone('Etc/UTC'));
|
||||
$this->enteredOn = new DateTimeImmutable('1/1/1970', new DateTimeZone('Etc/UTC'));
|
||||
}
|
||||
}
|
@ -3,10 +3,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal\Domain;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* An action that was taken on a request
|
||||
*/
|
||||
enum RequestAction implements \JsonSerializable
|
||||
enum RequestAction implements JsonSerializable
|
||||
{
|
||||
/** The request was entered */
|
||||
case Created;
|
98
src/app/lib/start.php
Normal file
98
src/app/lib/start.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 'On');
|
||||
|
||||
use Auth0\SDK\Auth0;
|
||||
use BitBadger\PgDocuments\Configuration;
|
||||
use DateTimeZone;
|
||||
use Dotenv\Dotenv;
|
||||
use MyPrayerJournal\Constants;
|
||||
|
||||
Dotenv::createImmutable(__DIR__)->load();
|
||||
|
||||
/** @var Auth0 The Auth0 instance to use for the request */
|
||||
$auth0 = new Auth0([
|
||||
'domain' => $_ENV[Constants::AUTH0_DOMAIN],
|
||||
'clientId' => $_ENV[Constants::AUTH0_CLIENT_ID],
|
||||
'clientSecret' => $_ENV[Constants::AUTH0_CLIENT_SECRET],
|
||||
'cookieSecret' => $_ENV[Constants::AUTH0_COOKIE_SECRET]
|
||||
]);
|
||||
|
||||
/** @var ?object The Auth0 session for the current user */
|
||||
$session = $auth0->getCredentials();
|
||||
if (!is_null($session)) $_REQUEST[Constants::USER_ID] = $session->user[Constants::CLAIM_SUB];
|
||||
|
||||
$_REQUEST[Constants::IS_HTMX] = array_key_exists(Constants::HEADER_HX_REQUEST, $_SERVER)
|
||||
&& (!array_key_exists(Constants::HEADER_HX_HIST_REQ, $_SERVER));
|
||||
|
||||
$_REQUEST[Constants::TIME_ZONE] = new DateTimeZone(
|
||||
array_key_exists(Constants::HEADER_USER_TZ, $_SERVER) ? $_SERVER[Constants::HEADER_USER_TZ] : 'Etc/UTC');
|
||||
|
||||
$_REQUEST[Constants::VERSION] = 4;
|
||||
|
||||
Configuration::$startUp = '\MyPrayerJournal\Data::startUp';
|
||||
|
||||
/**
|
||||
* Bring in a template
|
||||
*/
|
||||
function template(string $name)
|
||||
{
|
||||
require_once __DIR__ . "/../templates/$name.php";
|
||||
}
|
||||
|
||||
/**
|
||||
* If a user is not found, either redirect them or fail the request
|
||||
*
|
||||
* @param bool $fail Whether to fail the request (true) or redirect to log on (false - optional, default)
|
||||
*/
|
||||
function require_user(bool $fail = false)
|
||||
{
|
||||
if (!array_key_exists(Constants::USER_ID, $_REQUEST)) {
|
||||
if ($fail) {
|
||||
http_response_code(403);
|
||||
} else {
|
||||
header("Location: /user/log-on?{${Constants::RETURN_URL}}={$_SERVER[Constants::REQUEST_URI]}");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a bare header for a component result
|
||||
*/
|
||||
function bare_header()
|
||||
{
|
||||
echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf8"><title></title></head><body>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a traditional and htmx link, and apply an active class if the link is active
|
||||
*
|
||||
* @param string $url The URL of the page to be linked
|
||||
* @param array $classNames CSS class names to be applied to the link (optional, default none)
|
||||
* @param bool $checkActive Whether to apply an active class if the route matches (optional, default false)
|
||||
*/
|
||||
function page_link(string $url, array $classNames = [], bool $checkActive = false)
|
||||
{
|
||||
echo 'href="'. $url . '" hx-get="' . $url . '"';
|
||||
if ($checkActive && str_starts_with($_SERVER[Constants::REQUEST_URI], $url)) {
|
||||
array_push($classNames, 'is-active-route');
|
||||
}
|
||||
if (!empty($classNames)) {
|
||||
echo ' class="' . implode(' ', $classNames) . '"';
|
||||
}
|
||||
echo ' hx-target="#top" hx-swap="innerHTML" hx-push-url="true"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Close any open database connection; close the `body` and `html` tags
|
||||
*/
|
||||
function end_request()
|
||||
{
|
||||
Configuration::closeConn();
|
||||
echo '</body></html>';
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
<?php
|
||||
use MyPrayerJournal\Dates;
|
||||
|
||||
$spacer = '<span> </span>';
|
||||
/**
|
||||
* Format the activity and relative time
|
||||
*
|
||||
* @param string $activity The activity performed (activity or prayed)
|
||||
* @param \DateTimeImmutable $asOf The date/time the activity was performed
|
||||
*/
|
||||
function formatActivity(string $activity, \DateTimeImmutable $asOf)
|
||||
{
|
||||
echo "last $activity <span title=\"" . $asOf->setTimezone($_REQUEST['USER_TIME_ZONE'])->format('l, F jS, Y/g:ia T')
|
||||
. '">' . Dates::formatDistance(Dates::now(), $asOf) . '</span>';
|
||||
} ?>
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-header p-0 d-flex" role="tool-bar">
|
||||
<a <?php $page_link("/request/{$request->id}/edit"); ?> class="btn btn-secondary" title="Edit Request">
|
||||
<span class="material-icons">edit</span>
|
||||
</a><?php echo $spacer; ?>
|
||||
<button type="button" class="btn btn-secondary" title="Add Notes" data-bs-toggle="modal"
|
||||
data-bs-target="#notesModal" hx-get="/components/request/<?php echo $request->id; ?>/add-notes"
|
||||
hx-target="#notesBody" hx-swap="innerHTML">
|
||||
<span class="material-icons">comment</span>
|
||||
</button><?php echo $spacer; ?>
|
||||
<button type="button" class="btn btn-secondary" title="Snooze Request" data-bs-toggle="modal"
|
||||
data-bs-target="#snoozeModal" hx-get="/components/request/<?php echo $request->id; ?>/snooze"
|
||||
hx-target="#snoozeBody" hx-swap="innerHTML">
|
||||
<span class="material-icons">schedule</span>
|
||||
</button>
|
||||
<div class="flex-grow-1"></div>
|
||||
<button type="button" class="btn btn-success w-25" hx-patch="/request/<?php echo $request->id; ?>/prayed"
|
||||
title="Mark as Prayed">
|
||||
<span class="material-icons">done</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="request-text"><?php echo htmlentities($request->text); ?></p>
|
||||
</div>
|
||||
<div class="card-footer text-end text-muted px-1 py-0">
|
||||
<em><?php
|
||||
[ $activity, $asOf ] = is_null($request->lastPrayed)
|
||||
? [ 'activity', $request->asOf ]
|
||||
: [ 'prayed', $request->lastPrayed ];
|
||||
formatActivity($activity, $asOf); ?>
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
if (count($requests) == 0) {
|
||||
echo app()->template->render('components/no_results', [
|
||||
'heading' => 'No Active Requests',
|
||||
'link' => '/request/new/edit',
|
||||
'buttonText' => 'Add a Request',
|
||||
'text' => 'You have no requests to be shown; see the “Active” link above for snoozed or '
|
||||
. 'deferred requests, and the “Answered” link for answered requests'
|
||||
]);
|
||||
} else { ?>
|
||||
<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
|
||||
foreach ($requests as $request) {
|
||||
echo app()->template->render('components/journal_card', [ 'request' => $request ]);
|
||||
} ?>
|
||||
</section><?php
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
<div class="card">
|
||||
<h5 class="card-header"><?php echo $heading; ?></h5>
|
||||
<div class="card-body text-center">
|
||||
<p class="card-text"><?php echo $text; ?></p>
|
||||
<a <?php $page_link($link); ?> class="btn btn-primary"><?php echo $buttonText; ?></a>
|
||||
</div>
|
||||
</div>
|
@ -1,43 +0,0 @@
|
||||
<article class="container-fluid mt-3">
|
||||
<h2 class="pb-3"><?php echo $pageTitle; ?></h2>
|
||||
<p class="pb-3 text-center">
|
||||
<a <?php $page_link('/request/new/edit'); ?> class="btn btn-primary">
|
||||
<span class="material-icons">add_box</span> Add a Prayer Request
|
||||
</a>
|
||||
</p>
|
||||
<p hx-get="/components/journal-items" hx-swap="outerHTML" hx-trigger="load delay:.25s">
|
||||
Loading your prayer journal…
|
||||
</p>
|
||||
<div id="notesModal" class="modal fade" tabindex="-1" aria-labelled-by="nodesModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="nodesModalLabel">Add Notes to Prayer Request</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body" id="notesBody"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="notesDismiss" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="snoozeModal" class="modal fade" tabindex="-1" aria-labelled-by="snoozeModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="snoozeModalLabel">Snooze Prayer Request</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body" id="snoozeBody"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="snoozeDismiss" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
if (!$isHtmx) { ?>
|
||||
<footer class="container-fluid">
|
||||
<p class="text-muted text-end">
|
||||
myPrayerJournal <?= $version ?><br>
|
||||
<em><small>
|
||||
<a <?php $page_link('/legal/privacy-policy'); ?>>Privacy Policy</a> •
|
||||
<a <?php $page_link('/legal/terms-of-service'); ?>>Terms of Service</a> •
|
||||
<a href="https://github.com/bit-badger/myprayerjournal" target="_blank" rel="noopener">Developed</a>
|
||||
and hosted by
|
||||
<a href="https://bitbadger.solutions" target="_blank" rel="noopener">Bit Badger Solutions</a>
|
||||
</small></em>
|
||||
</p>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.4"
|
||||
integrity="sha384-zUfuhFKKZCbHTY6aRR46gxiqszMk5tcHjsVFxnUo8VMus4kHGVdIYVbOYYNlKmHV"
|
||||
crossorigin="anonymous"></script>
|
||||
<!-- script [] [
|
||||
rawText "if (!htmx) document.write('<script src=\"/script/htmx.min.js\"><\/script>')"
|
||||
] -->
|
||||
<script async src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
||||
crossorigin="anonymous"></script>
|
||||
<!-- script [] [
|
||||
rawText "setTimeout(function () { "
|
||||
rawText "if (!bootstrap) document.write('<script src=\"/script/bootstrap.bundle.min.js\"><\/script>') "
|
||||
rawText "}, 2000)"
|
||||
] -->
|
||||
<script src="/script/mpj.js"></script>
|
||||
</footer><?php
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<head>
|
||||
<title><?php echo $pageTitle; ?> « myPrayerJournal</title><?php
|
||||
if (!$isHtmx) { ?>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Online prayer journal - free w/Google or Microsoft account">
|
||||
<link href= "https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx"
|
||||
crossorigin="anonymous">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="/style/style.css" rel="stylesheet"><?php
|
||||
} ?>
|
||||
</head>
|
@ -1,21 +0,0 @@
|
||||
<nav class="navbar navbar-dark" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<a <?php $page_link('/'); ?> class="navbar-brand">
|
||||
<span class="m">my</span><span class="p">Prayer</span><span class="j">Journal</span>
|
||||
</a>
|
||||
<ul class="navbar-nav me-auto d-flex flex-row"><?php
|
||||
if ($user) { ?>
|
||||
<li class="nav-item"><a <?php $page_link('/journal', true); ?>>Journal</a></li>
|
||||
<li class="nav-item"><a <?php $page_link('/requests/active', true); ?>>Active</a></li><?php
|
||||
if ($hasSnoozed) { ?>
|
||||
<li class="nav-item"><a <?php $page_link('/requests/snoozed', true); ?>>Snoozed</a></li><?php
|
||||
} ?>
|
||||
<li class="nav-item"><a <?php $page_link('/requests/answered', true); ?>>Answered</a></li>
|
||||
<li class="nav-item"><a href="/user/log-off">Log Off</a></li><?php
|
||||
} else { ?>
|
||||
<li class="nav-item"><a href="/user/log-on">Log On</a></li><?php
|
||||
} ?>
|
||||
<li class="nav-item"><a href="https://docs.prayerjournal.me" target="_blank" rel="noopener">Docs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
@ -1,5 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title></title></head>
|
||||
<body><?php echo $pageContent; ?></body>
|
||||
</html>
|
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<?php echo app()->template->render('layout/_head', [ 'pageTitle' => $pageTitle, 'isHtmx' => $isHtmx ]); ?>
|
||||
<body>
|
||||
<section id="top" aria-label="Top navigation">
|
||||
<?php echo app()->template->render('layout/_nav', [ 'user' => $user, 'hasSnoozed' => $hasSnoozed ]); ?>
|
||||
<main role="main"><?php echo $pageContent; ?></main>
|
||||
</section>
|
||||
<?php echo app()->template->render('layout/_foot', [ 'isHtmx' => $isHtmx ]); ?>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><?php echo $pageTitle; ?> « myPrayerJournal</title>
|
||||
</head>
|
||||
<body>
|
||||
<section id="top" aria-label="Top navigation">
|
||||
<?php echo app()->template->render('layout/_nav', [ 'user' => $user, 'hasSnoozed' => $hasSnoozed ]); ?>
|
||||
<main role="main"><?php echo $pageContent; ?></main>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
@ -1,82 +0,0 @@
|
||||
<article class="container mt-3">
|
||||
<h2 class="mb-2">Privacy Policy</h2>
|
||||
<h6 class="text-muted pb-3">as of May 21<sup>st</sup>, 2018</h6>
|
||||
<p>
|
||||
The nature of the service is one where privacy is a must. The items below will help you understand the data we
|
||||
collect, access, and store on your behalf as you use this service.
|
||||
</p>
|
||||
<div class="card">
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item">
|
||||
<h3>Third Party Services</h3>
|
||||
<p class="card-text">
|
||||
myPrayerJournal utilizes a third-party authentication and identity provider. You should familiarize
|
||||
yourself with the privacy policy for
|
||||
<a href="https://auth0.com/privacy" target="_blank" rel="noopener">Auth0</a>, as well as
|
||||
your chosen provider
|
||||
(<a href="https://privacy.microsoft.com/en-us/privacystatement" target="_blank"
|
||||
rel="noopener">Microsoft</a> or
|
||||
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener">Google</a>).
|
||||
</p>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<h3>What We Collect</h3>
|
||||
<h4>Identifying Data</h4>
|
||||
<ul>
|
||||
<li>
|
||||
The only identifying data myPrayerJournal stores is the subscriber (“sub”) field
|
||||
from the token we receive from Auth0, once you have signed in through their hosted service. All
|
||||
information is associated with you via this field.
|
||||
</li>
|
||||
<li>
|
||||
While you are signed in, within your browser, the service has access to your first and last
|
||||
names, along with a URL to the profile picture (provided by your selected identity provider).
|
||||
This information is not transmitted to the server, and is removed when “Log Off” is
|
||||
clicked.
|
||||
</li>
|
||||
</ul>
|
||||
<h4>User Provided Data</h4>
|
||||
<ul class="mb-0">
|
||||
<li>
|
||||
myPrayerJournal stores the information you provide, including the text of prayer requests,
|
||||
updates, and notes; and the date/time when certain actions are taken.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<h3>How Your Data Is Accessed / Secured</h3>
|
||||
<ul class="mb-0">
|
||||
<li>
|
||||
Your provided data is returned to you, as required, to display your journal or your answered
|
||||
requests. On the server, it is stored in a controlled-access database.
|
||||
</li>
|
||||
<li>
|
||||
Your data is backed up, along with other Bit Badger Solutions hosted systems, in a rolling
|
||||
manner; backups are preserved for the prior 7 days, and backups from the 1<sup>st</sup> and
|
||||
15<sup>th</sup> are preserved for 3 months. These backups are stored in a private cloud data
|
||||
repository.
|
||||
</li>
|
||||
<li>
|
||||
The data collected and stored is the absolute minimum necessary for the functionality of the
|
||||
service. There are no plans to “monetize” this service, and storing the minimum
|
||||
amount of information means that the data we have is not interesting to purchasers (or those who
|
||||
may have more nefarious purposes).
|
||||
</li>
|
||||
<li>
|
||||
Access to servers and backups is strictly controlled and monitored for unauthorized access
|
||||
attempts.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<h3>Removing Your Data</h3>
|
||||
<p class="card-text">
|
||||
At any time, you may choose to discontinue using this service. Both Microsoft and Google provide
|
||||
ways to revoke access from this application. However, if you want your data removed from the
|
||||
database, please contact daniel at bitbadger.solutions (via e-mail, replacing at with @) prior to
|
||||
doing so, to ensure we can determine which subscriber ID belongs to you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
@ -1,57 +0,0 @@
|
||||
<article class="container mt-3">
|
||||
<h2 class="mb-2">Terms of Service</h2>
|
||||
<h6 class="text-muted pb-3">as of May 21<sup>st</sup>, 2018</h6>
|
||||
<div class="card">
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item">
|
||||
<h3>1. Acceptance of Terms</h3>
|
||||
<p class="card-text">
|
||||
By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you
|
||||
are responsible to ensure that your use of this site complies with all applicable laws. Your
|
||||
continued use of this site implies your acceptance of these terms.
|
||||
</p>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<h3>2. Description of Service and Registration</h3>
|
||||
<p class="card-text">
|
||||
myPrayerJournal is a service that allows individuals to enter and amend their prayer requests. It
|
||||
requires no registration by itself, but access is granted based on a successful login with an
|
||||
external identity provider. See
|
||||
<a <?php $page_link('/legal/privacy-policy'); ?>>our privacy policy</a> for details on how that
|
||||
information is accessed and stored.
|
||||
</p>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<h3>3. Third Party Services</h3>
|
||||
<p class="card-text">
|
||||
This service utilizes a third-party service provider for identity management. Review the terms of
|
||||
service for <a href="https://auth0.com/terms" target="_blank" rel="noopener">Auth0</a>, as well as
|
||||
those for the selected authorization provider
|
||||
(<a href="https://www.microsoft.com/en-us/servicesagreement" target="_blank"
|
||||
rel="noopener">Microsoft</a> or
|
||||
<a href="https://policies.google.com/terms" target="_blank" rel="noopener">Google</a>).
|
||||
</p>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<h3>4. Liability</h3>
|
||||
<p class="card-text">
|
||||
This service is provided “as is”, and no warranty (express or implied) exists. The
|
||||
service and its developers may not be held liable for any damages that may arise through the use of
|
||||
this service.
|
||||
</p>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<h3>5. Updates to Terms</h3>
|
||||
<p class="card-text">
|
||||
These terms and conditions may be updated at any time, and this service does not have the capability
|
||||
to notify users when these change. The date at the top of the page will be updated when any of the
|
||||
text of these terms is updated.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="pt-3">
|
||||
You may also wish to review our <a <?php $page_link('/legal/privacy-policy'); ?>>privacy policy</a> to learn how
|
||||
we handle your data.
|
||||
</p>
|
||||
</article>
|
@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
use MyPrayerJournal\Domain\RecurrenceType;
|
||||
|
||||
$cancelLink = match ($returnTo) {
|
||||
'active' => '/requests/active',
|
||||
'snoozed' => '/requests/snoozed',
|
||||
default => '/journal',
|
||||
};
|
||||
$isImmediate = $request->recurrenceType == RecurrenceType::Immediate;
|
||||
$isHours = $request->recurrenceType == RecurrenceType::Hours;
|
||||
$isDays = $request->recurrenceType == RecurrenceType::Days;
|
||||
$isWeeks = $request->recurrenceType == RecurrenceType::Weeks; ?>
|
||||
<article class="container">
|
||||
<h2 class="pb-3"><?php echo $isNew ? 'Add' : 'Edit'; ?> Prayer Request</h2>
|
||||
<form hx-boost="true" hx-target="#top" hx-push-url="true"
|
||||
<?php echo $isNew ? 'hx-post' : 'hx-patch'; ?>="/request">
|
||||
<input type="hidden" name="requestId" value="<?php echo $isNew ? 'new' : $request->id; ?>">
|
||||
<input type="hidden" name="returnTo" value="<?php echo $returnTo; ?>">
|
||||
<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><?php echo $request->text; ?></textarea>
|
||||
<label for="requestText">Prayer Request</label>
|
||||
</div><br><?php
|
||||
if (!$isNew) { ?>
|
||||
<div class="pb-3">
|
||||
<label>Also Mark As</label><br>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" class="form-check-input" id="sU" name="status" value="Updated" checked>
|
||||
<label for="sU">Updated</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" class="form-check-input" id="sP" name="status" value="Prayed">
|
||||
<label for="sP">Prayed</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" class="form-check-input" id="sA" name="status" value="Answered">
|
||||
<label for="sA">Answered</label>
|
||||
</div>
|
||||
</div><?php
|
||||
} ?>
|
||||
<div class="row">
|
||||
<div class="col-12 offset-md-2 col-md-8 offset-lg-3 col-lg-6">
|
||||
<p>
|
||||
<strong>Recurrence</strong>
|
||||
<em class="text-muted">After prayer, request reappears…</em>
|
||||
</p>
|
||||
<div class="d-flex flex-row flex-wrap justify-content-center align-items-center">
|
||||
<div class="form-check mx-2">
|
||||
<input type="radio" class="form-check-input" id="rI" name="recurType" value="Immediate"
|
||||
onclick="mpj.edit.toggleRecurrence(event)" <?php echo $isImmediate ? 'checked' : ''; ?>>
|
||||
<label for="rI">Immediately</label>
|
||||
</div>
|
||||
<div class="form-check mx-2">
|
||||
<input type="radio" class="form-check-input" id="rO" name="recurType" value="Other"
|
||||
onclick="mpj.edit.toggleRecurrence(event)" <?php echo $isImmediate ? '' : 'checked'; ?>>
|
||||
<label for="rO">Every…</label>
|
||||
</div>
|
||||
<div class="form-floating mx-2">
|
||||
<input type="number" class="form-control" id="recurCount" name="recurCount" placeholder="0"
|
||||
value="<?php echo $request->recurrence ?? 0; ?>" style="width:6rem;" required
|
||||
<?php echo $isImmediate ? '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 echo $isImmediate ? 'disabled' : ''; ?>>
|
||||
<option value="Hours" <?php echo $isHours ? 'selected' : ''; ?>>hours</option>
|
||||
<option value="Days" <?php echo $isDays ? 'selected' : ''; ?>>days</option>
|
||||
<option value="Weeks" <?php echo $isWeeks ? 'selected' : ''; ?>>weeks</option>
|
||||
</select>
|
||||
<label for="recurInterval">Interval</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end pt-3">
|
||||
<button class="btn btn-primary me-2" type="submit"><span class="material-icons">save</span> Save</button>
|
||||
<a <?php $page_link($cancelLink); ?> class="btn btn-secondary ms-2">
|
||||
<span class="material-icons">arrow_back</span> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
93
src/app/public/components/journal-items.php
Normal file
93
src/app/public/components/journal-items.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once '../../lib/start.php';
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MyPrayerJournal\{ Constants, Data, Dates };
|
||||
use MyPrayerJournal\Domain\JournalRequest;
|
||||
|
||||
require_user(true);
|
||||
|
||||
$requests = Data::getJournal($_REQUEST[Constants::USER_ID]);
|
||||
|
||||
bare_header();
|
||||
if ($requests) { ?>
|
||||
<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">
|
||||
<p class="mb-3 has-text-centered">
|
||||
<a <?php page_link('/request/edit?new'); ?> class="button is-light">
|
||||
<span class="material-icons">add_box</span> Add a Prayer Request
|
||||
</a>
|
||||
</p><?php
|
||||
array_walk($requests, journal_card(...)); ?>
|
||||
</section><?php
|
||||
} else {
|
||||
$_REQUEST['EMPTY_HEADING'] = 'No Active Requests';
|
||||
$_REQUEST['EMPTY_LINK'] = '/request/edit?new';
|
||||
$_REQUEST['EMPTY_BTN_TXT'] = 'Add a Request';
|
||||
$_REQUEST['EMPTY_TEXT'] = 'You have no requests to be shown; see the “Active” link above for '
|
||||
. 'snoozed or deferred requests, and the “Answered” link for answered requests';
|
||||
template('no_content');
|
||||
}
|
||||
end_request();
|
||||
|
||||
/**
|
||||
* Format the activity and relative time
|
||||
*
|
||||
* @param string $activity The activity performed (activity or prayed)
|
||||
* @param DateTimeImmutable $asOf The date/time the activity was performed
|
||||
*/
|
||||
function format_activity(string $activity, DateTimeImmutable $asOf)
|
||||
{
|
||||
echo sprintf('last %s <span title="%s">%s</span>', [
|
||||
$activity,
|
||||
$asOf->setTimezone($_REQUEST[Constants::TIME_ZONE])->format('l, F jS, Y/g:ia T'),
|
||||
Dates::formatDistance(Dates::now(), $asOf)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a card for a prayer request
|
||||
*
|
||||
* @param JournalRequest $req The request for which a card should be generated
|
||||
*/
|
||||
function journal_card(JournalRequest $req)
|
||||
{
|
||||
$spacer = '<span> </span>'; ?>
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-header p-0 d-flex" role="tool-bar">
|
||||
<a <?php page_link("/request/{$req->id}/edit"); ?> class="btn btn-secondary" title="Edit Request">
|
||||
<span class="material-icons">edit</span>
|
||||
</a><?php echo $spacer; ?>
|
||||
<button type="button" class="btn btn-secondary" title="Add Notes" data-bs-toggle="modal"
|
||||
data-bs-target="#notesModal" hx-get="/components/request/<?php echo $req->id; ?>/add-notes"
|
||||
hx-target="#notesBody" hx-swap="innerHTML">
|
||||
<span class="material-icons">comment</span>
|
||||
</button><?php echo $spacer; ?>
|
||||
<button type="button" class="btn btn-secondary" title="Snooze Request" data-bs-toggle="modal"
|
||||
data-bs-target="#snoozeModal" hx-get="/components/request/<?php echo $req->id; ?>/snooze"
|
||||
hx-target="#snoozeBody" hx-swap="innerHTML">
|
||||
<span class="material-icons">schedule</span>
|
||||
</button>
|
||||
<div class="flex-grow-1"></div>
|
||||
<button type="button" class="btn btn-success w-25" hx-patch="/request/<?php echo $req->id; ?>/prayed"
|
||||
title="Mark as Prayed">
|
||||
<span class="material-icons">done</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="request-text"><?php echo htmlentities($req->text); ?></p>
|
||||
</div>
|
||||
<div class="card-footer text-end text-muted px-1 py-0">
|
||||
<em><?php
|
||||
[ $activity, $asOf ] = is_null($req->lastPrayed)
|
||||
? [ 'activity', $req->asOf ]
|
||||
: [ 'prayed', $req->lastPrayed ];
|
||||
format_activity($activity, $asOf); ?>
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php
|
||||
}
|
@ -1,13 +1,25 @@
|
||||
<article class="container mt-3">
|
||||
<p> </p>
|
||||
<p>
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once '../lib/start.php';
|
||||
|
||||
use MyPrayerJournal\Constants;
|
||||
|
||||
$_REQUEST[Constants::PAGE_TITLE] = 'Welcome';
|
||||
|
||||
template('layout/page_header'); ?>
|
||||
<main class="container">
|
||||
<p class="block"> </p>
|
||||
<p class="block">
|
||||
myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them,
|
||||
update them as God moves in the situation, and record a final answer received on that request. It also allows
|
||||
individuals to review their answered prayers.
|
||||
</p>
|
||||
<p>
|
||||
<p class="block">
|
||||
This site is open and available to the general public. To get started, simply click the “Log On”
|
||||
link above, and log on with either a Microsoft or Google account. You can also learn more about the site at the
|
||||
“Docs” link, also above.
|
||||
</p>
|
||||
</article>
|
||||
</main><?php
|
||||
template('layout/page_footer');
|
||||
end_request();
|
21
src/app/public/journal.php
Normal file
21
src/app/public/journal.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once '../lib/start.php';
|
||||
|
||||
use MyPrayerJournal\Constants;
|
||||
|
||||
require_user();
|
||||
|
||||
$_REQUEST[Constants::PAGE_TITLE] = "{$session->user[Constants::CLAIM_GIVEN_NAME]}’s Prayer Journal";
|
||||
|
||||
template('layout/page_header'); ?>
|
||||
<main class="container">
|
||||
<h2 class="title"><?php echo $_REQUEST[Constants::PAGE_TITLE]; ?>’s Prayer Journal</h2>
|
||||
<p hx-get="/components/journal-items" hx-swap="outerHTML" hx-trigger="load delay:.25s">
|
||||
Loading your prayer journal…
|
||||
</p>
|
||||
<pre><?php var_dump($_SERVER); ?></pre>
|
||||
</main><?php
|
||||
template('layout/page_footer');
|
||||
end_request();
|
98
src/app/public/legal/privacy-policy.php
Normal file
98
src/app/public/legal/privacy-policy.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once '../../lib/start.php';
|
||||
|
||||
use MyPrayerJournal\Constants;
|
||||
|
||||
$_REQUEST[Constants::PAGE_TITLE] = 'Privacy Policy';
|
||||
|
||||
template('layout/page_header'); ?>
|
||||
<main class="container">
|
||||
<h2 class="title">Privacy Policy</h2>
|
||||
<h6 class="subtitle">as of May 21<sup>st</sup>, 2018</h6>
|
||||
<p class="mb-3">
|
||||
The nature of the service is one where privacy is a must. The items below will help you understand the data we
|
||||
collect, access, and store on your behalf as you use this service.
|
||||
</p>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h3 class="card-header-title">Third Party Services</h3>
|
||||
</div>
|
||||
<p class="card-content">
|
||||
myPrayerJournal utilizes a third-party authentication and identity provider. You should familiarize yourself
|
||||
with the privacy policy for
|
||||
<a href="https://auth0.com/privacy" target="_blank" rel="noopener">Auth0</a>, as well as your chosen
|
||||
provider
|
||||
(<a href="https://privacy.microsoft.com/en-us/privacystatement" target="_blank"
|
||||
rel="noopener">Microsoft</a> or
|
||||
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener">Google</a>).
|
||||
</p>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h3 class="card-header-title">What We Collect</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h4 class="subtitle mb-3">Identifying Data</h4>
|
||||
<ul class="mb-3 mx-5">
|
||||
<li>
|
||||
• The only identifying data myPrayerJournal stores is the subscriber (“sub”) field
|
||||
from the token we receive from Auth0, once you have signed in through their hosted service. All
|
||||
information is associated with you via this field.
|
||||
</li>
|
||||
<li>
|
||||
• While you are signed in, within your browser, the service has access to your first and last
|
||||
names, along with a URL to the profile picture (provided by your selected identity provider). This
|
||||
information is not transmitted to the server, and is removed when “Log Off” is clicked.
|
||||
</li>
|
||||
</ul>
|
||||
<h4 class="subtitle mb-3">User Provided Data</h4>
|
||||
<ul class="mx-5">
|
||||
<li>
|
||||
• myPrayerJournal stores the information you provide, including the text of prayer requests,
|
||||
updates, and notes; and the date/time when certain actions are taken.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h3 class="card-header-title">How Your Data Is Accessed / Secured</h3>
|
||||
</div>
|
||||
<ul class="card-content">
|
||||
<li>
|
||||
• Your provided data is returned to you, as required, to display your journal or your answered
|
||||
requests. On the server, it is stored in a controlled-access database.
|
||||
</li>
|
||||
<li>
|
||||
• Your data is backed up, along with other Bit Badger Solutions hosted systems, in a rolling
|
||||
manner; backups are preserved for the prior 7 days, and backups from the 1<sup>st</sup> and
|
||||
15<sup>th</sup> are preserved for 3 months. These backups are stored in a private cloud data repository.
|
||||
</li>
|
||||
<li>
|
||||
• The data collected and stored is the absolute minimum necessary for the functionality of the
|
||||
service. There are no plans to “monetize” this service, and storing the minimum amount of
|
||||
information means that the data we have is not interesting to purchasers (or those who may have more
|
||||
nefarious purposes).
|
||||
</li>
|
||||
<li>
|
||||
• Access to servers and backups is strictly controlled and monitored for unauthorized access
|
||||
attempts.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h3 class="card-header-title">Removing Your Data</h3>
|
||||
</div>
|
||||
<p class="card-content">
|
||||
At any time, you may choose to discontinue using this service. Both Microsoft and Google provide ways to
|
||||
revoke access from this application. However, if you want your data removed from the database, please
|
||||
contact daniel at bitbadger.solutions (via e-mail, replacing at with @) prior to doing so, to ensure we can
|
||||
determine which subscriber ID belongs to you.
|
||||
</p>
|
||||
</div>
|
||||
</main><?php
|
||||
template('layout/page_footer');
|
||||
end_request();
|
73
src/app/public/legal/terms-of-service.php
Normal file
73
src/app/public/legal/terms-of-service.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once '../../lib/start.php';
|
||||
|
||||
use MyPrayerJournal\Constants;
|
||||
|
||||
$_REQUEST[Constants::PAGE_TITLE] = 'Terms of Service';
|
||||
|
||||
template('layout/page_header'); ?>
|
||||
<main class="container">
|
||||
<h2 class="title">Terms of Service</h2>
|
||||
<h6 class="subtitle">as of May 21<sup>st</sup>, 2018</h6>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h3 class="card-header-title">1. Acceptance of Terms</h3>
|
||||
</div>
|
||||
<p class="card-content">
|
||||
By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you are
|
||||
responsible to ensure that your use of this site complies with all applicable laws. Your continued use of
|
||||
this site implies your acceptance of these terms.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h3 class="card-header-title">2. Description of Service and Registration</h3>
|
||||
</div>
|
||||
<p class="card-content">
|
||||
myPrayerJournal is a service that allows individuals to enter and amend their prayer requests. It requires
|
||||
no registration by itself, but access is granted based on a successful login with an external identity
|
||||
provider. See <a <?php page_link('/legal/privacy-policy'); ?>>our privacy policy</a> for details on how that
|
||||
information is accessed and stored.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h3 class="card-header-title">3. Third Party Services</h3>
|
||||
</div>
|
||||
<p class="card-content">
|
||||
This service utilizes a third-party service provider for identity management. Review the terms of service
|
||||
for <a href="https://auth0.com/terms" target="_blank" rel="noopener">Auth0</a>, as well as those for the
|
||||
selected authorization provider
|
||||
(<a href="https://www.microsoft.com/en-us/servicesagreement" target="_blank"
|
||||
rel="noopener">Microsoft</a> or
|
||||
<a href="https://policies.google.com/terms" target="_blank" rel="noopener">Google</a>).
|
||||
</p>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h3 class="card-header-title">4. Liability</h3>
|
||||
</div>
|
||||
<p class="card-content">
|
||||
This service is provided “as is”, and no warranty (express or implied) exists. The service and
|
||||
its developers may not be held liable for any damages that may arise through the use of this service.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h3 class="card-header-title">5. Updates to Terms</h3>
|
||||
</div>
|
||||
<p class="card-content">
|
||||
These terms and conditions may be updated at any time, and this service does not have the capability to
|
||||
notify users when these change. The date at the top of the page will be updated when any of the text of
|
||||
these terms is updated.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
You may also wish to review our <a <?php page_link('/legal/privacy-policy'); ?>>privacy policy</a> to learn how
|
||||
we handle your data.
|
||||
</p>
|
||||
</main><?php
|
||||
template('layout/page_footer');
|
||||
end_request();
|
@ -1,5 +1,5 @@
|
||||
|
||||
nav {
|
||||
nav.navbar.is-dark {
|
||||
background-color: green;
|
||||
|
||||
& .m {
|
9
src/app/public/user/log-off.php
Normal file
9
src/app/public/user/log-off.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once '../../lib/start.php';
|
||||
|
||||
use MyPrayerJournal\Constants;
|
||||
|
||||
header("Location: {$auth0->logout($_ENV[Constants::BASE_URL])}");
|
||||
exit;
|
24
src/app/public/user/log-on.php
Normal file
24
src/app/public/user/log-on.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once '../../lib/start.php';
|
||||
|
||||
use MyPrayerJournal\Constants;
|
||||
|
||||
$auth0->clear();
|
||||
|
||||
// Check for return URL; if present, store it in a cookie we'll retrieve when we're logged in
|
||||
$nonce = '';
|
||||
if (array_key_exists(Constants::RETURN_URL, $_GET)) {
|
||||
$nonce = urlencode(base64_encode(openssl_random_pseudo_bytes(8)));
|
||||
setcookie(Constants::COOKIE_REDIRECT, "$nonce|{$_GET[Constants::RETURN_URL]}", [
|
||||
'expires' => -1,
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
]);
|
||||
}
|
||||
$params = $nonce ? [ Constants::LOG_ON_STATE => $nonce ] : [];
|
||||
|
||||
header('Location: ' . $auth0->login("{$_ENV[Constants::BASE_URL]}/user/logged-on", $params));
|
||||
exit;
|
26
src/app/public/user/logged-on.php
Normal file
26
src/app/public/user/logged-on.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once '../../lib/start.php';
|
||||
|
||||
use MyPrayerJournal\Constants;
|
||||
|
||||
$auth0->exchange("{$_ENV[Constants::BASE_URL]}/user/logged-on");
|
||||
|
||||
$nextUrl = '/journal';
|
||||
if (array_key_exists(Constants::LOG_ON_STATE, $_GET)) {
|
||||
$nonce = base64_decode(urldecode($_GET[Constants::LOG_ON_STATE]));
|
||||
[$verify, $newNext] = explode('|', $_COOKIE[Constants::COOKIE_REDIRECT]);
|
||||
if ($verify == $nonce && $newNext && str_starts_with($newNext, '/') && !str_starts_with($newNext, '//')) {
|
||||
$nextUrl = $newNext;
|
||||
}
|
||||
}
|
||||
|
||||
setcookie(Constants::COOKIE_REDIRECT, '', [
|
||||
'expires' => -1,
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
]);
|
||||
header("Location: $nextUrl");
|
||||
exit;
|
21
src/app/templates/layout/page_footer.php
Normal file
21
src/app/templates/layout/page_footer.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
use MyPrayerJournal\Constants; ?>
|
||||
</section><?php
|
||||
if (!$_REQUEST[Constants::IS_HTMX]) { ?>
|
||||
<footer class="container-fluid mx-1">
|
||||
<p class="text-muted has-text-right">
|
||||
myPrayerJournal <?php echo $_REQUEST[Constants::VERSION]; ?><br>
|
||||
<em><small>
|
||||
<a <?php page_link('/legal/privacy-policy'); ?>>Privacy Policy</a> •
|
||||
<a <?php page_link('/legal/terms-of-service'); ?>>Terms of Service</a> •
|
||||
<a href="https://github.com/bit-badger/myprayerjournal" target="_blank" rel="noopener">Developed</a>
|
||||
and hosted by
|
||||
<a href="https://bitbadger.solutions" target="_blank" rel="noopener">Bit Badger Solutions</a>
|
||||
</small></em>
|
||||
</p>
|
||||
</footer>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.4"
|
||||
integrity="sha384-zUfuhFKKZCbHTY6aRR46gxiqszMk5tcHjsVFxnUo8VMus4kHGVdIYVbOYYNlKmHV"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="/script/mpj.js"></script><?php
|
||||
}
|
50
src/app/templates/layout/page_header.php
Normal file
50
src/app/templates/layout/page_header.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
use BitBadger\PgDocuments\Document;
|
||||
use MyPrayerJournal\{ Constants, Data };
|
||||
|
||||
$isLoggedOn = array_key_exists('MPJ_USER_ID', $_REQUEST);
|
||||
$hasSnoozed = false;
|
||||
if ($isLoggedOn) {
|
||||
$hasSnoozed = Document::countByJsonPath(Data::REQ_TABLE, '$.snoozedUntil ? (@ == "0")') > 0;
|
||||
}
|
||||
|
||||
$theTitle = array_key_exists(Constants::PAGE_TITLE, $_REQUEST) ? "{$_REQUEST[Constants::PAGE_TITLE]} « " : ''; ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title><?php echo $theTitle; ?>myPrayerJournal</title><?php
|
||||
if (!$_REQUEST[Constants::IS_HTMX]) { ?>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.min.css"
|
||||
integrity="sha512-HqxHUkJM0SYcbvxUw5P60SzdOTy/QVwA1JJrvaXJv4q7lmbDZCmZaqz01UPOaQveoxfYRv1tHozWGPMcuTBuvQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="/style/style.css" rel="stylesheet"><?php
|
||||
} ?>
|
||||
</head>
|
||||
<body>
|
||||
<section id="top" aria-label="top navigation">
|
||||
<nav class="navbar is-dark has-shadow" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a <?php page_link('/'); ?> class="navbar-item">
|
||||
<span class="m">my</span><span class="p">Prayer</span><span class="j">Journal</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start"><?php
|
||||
if ($isLoggedOn) { ?>
|
||||
<a <?php page_link('/journal', ['navbar-item'], true); ?>>Journal</a>
|
||||
<a <?php page_link('/requests/active', ['navbar-item'], true); ?>>Active</a><?php
|
||||
if ($hasSnoozed) { ?>
|
||||
<a <?php page_link('/requests/snoozed', ['navbar-item'], true); ?>>Snoozed</a><?php
|
||||
} ?>
|
||||
<a <?php page_link('/requests/answered', ['navbar-item'], true); ?>>Answered</a>
|
||||
<a href="/user/log-off" class="navbar-item">Log Off</a><?php
|
||||
} else { ?>
|
||||
<a href="/user/log-on" class="navbar-item">Log On</a><?php
|
||||
} ?>
|
||||
<a href="https://docs.prayerjournal.me" class="navbar-item" target="_blank" rel="noopener">Docs</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
10
src/app/templates/no_content.php
Normal file
10
src/app/templates/no_content.php
Normal file
@ -0,0 +1,10 @@
|
||||
<div class="card mb-3">
|
||||
<div class="card-header has-background-light">
|
||||
<h5 class="card-header-title"><?php echo $_REQUEST['EMPTY_HEADING']; ?></h5>
|
||||
</div>
|
||||
<div class="card-content has-text-centered">
|
||||
<p class="mb-5"><?php echo $_REQUEST['EMPTY_TEXT']; ?></p>
|
||||
<a <?php page_link($_REQUEST['EMPTY_LINK']); ?>
|
||||
class="button is-link"><?php echo $_REQUEST['EMPTY_BTN_TXT']; ?></a>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user