WIP on vanilla PHP framework

This commit is contained in:
Daniel J. Summers 2024-06-20 23:06:31 -04:00
parent 24c503385e
commit 41853a7645
16 changed files with 562 additions and 6 deletions

3
.gitignore vendored
View File

@ -256,3 +256,6 @@ paket-files/
.ionide
src/environment.txt
# PHP ignore files
src/vendor

View File

@ -8,9 +8,4 @@ myPrayerJournal was borne of out of a personal desire [Daniel](https://github.co
## Further Reading
The documentation for the site is at <https://bit-badger.github.io/myPrayerJournal/>.
---
_Thanks to [JetBrains](https://jb.gg/OpenSource) for licensing their awesome toolset to this project._
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo" width="100" height="100">](https://jb.gg/OpenSource)
The documentation for the site is at <https://prayerjournal.me/docs>.

16
src/composer.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "bit-badger/my-prayer-journal",
"minimum-stability": "dev",
"require": {
"php": ">=8.2",
"ext-pdo": "*",
"ext-sqlite3": "*",
"bit-badger/pdo-document": "^1",
"visus/cuid2": "^4"
},
"autoload": {
"psr-4": {
"MyPrayerJournal\\": "lib/"
}
}
}

182
src/composer.lock generated Normal file
View File

@ -0,0 +1,182 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "aa94d32d1f21c70c88434971a9dba5a8",
"packages": [
{
"name": "bit-badger/pdo-document",
"version": "v1.0.0-alpha2",
"source": {
"type": "git",
"url": "https://git.bitbadger.solutions/bit-badger/pdo-document",
"reference": "330e27218756df8b93081a17dead8aaec789b071"
},
"require": {
"ext-pdo": "*",
"netresearch/jsonmapper": "^4",
"php": ">=8.2"
},
"require-dev": {
"phpunit/phpunit": "^11"
},
"type": "library",
"autoload": {
"psr-4": {
"BitBadger\\PDODocument\\": "./src",
"BitBadger\\PDODocument\\Query\\": "./src/Query",
"BitBadger\\PDODocument\\Mapper\\": "./src/Mapper"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel J. Summers",
"email": "daniel@bitbadger.solutions",
"homepage": "https://bitbadger.solutions",
"role": "Developer"
}
],
"description": "Treat SQLite (and soon PostgreSQL) as a document store",
"keywords": [
"database",
"document",
"pdo",
"sqlite"
],
"support": {
"email": "daniel@bitbadger.solutions",
"rss": "https://git.bitbadger.solutions/bit-badger/pdo-document.rss",
"source": "https://git.bitbadger.solutions/bit-badger/pdo-document"
},
"time": "2024-06-11T11:07:56+00:00"
},
{
"name": "netresearch/jsonmapper",
"version": "v4.4.1",
"source": {
"type": "git",
"url": "https://github.com/cweiske/jsonmapper.git",
"reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0",
"reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-spl": "*",
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0",
"squizlabs/php_codesniffer": "~3.5"
},
"type": "library",
"autoload": {
"psr-0": {
"JsonMapper": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"OSL-3.0"
],
"authors": [
{
"name": "Christian Weiske",
"email": "cweiske@cweiske.de",
"homepage": "http://github.com/cweiske/jsonmapper/",
"role": "Developer"
}
],
"description": "Map nested JSON structures onto PHP classes",
"support": {
"email": "cweiske@cweiske.de",
"issues": "https://github.com/cweiske/jsonmapper/issues",
"source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1"
},
"time": "2024-01-31T06:18:54+00:00"
},
{
"name": "visus/cuid2",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/visus-io/php-cuid2.git",
"reference": "17c9b3098d556bb2556a084c948211333cc19c79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/visus-io/php-cuid2/zipball/17c9b3098d556bb2556a084c948211333cc19c79",
"reference": "17c9b3098d556bb2556a084c948211333cc19c79",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.29",
"ext-ctype": "*",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "^3.7",
"vimeo/psalm": "^5.4"
},
"suggest": {
"ext-gmp": "*"
},
"type": "library",
"autoload": {
"files": [
"src/compat.php"
],
"psr-4": {
"Visus\\Cuid2\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alan Brault",
"email": "alan.brault@visus.io"
}
],
"description": "A PHP library for generating collision-resistant ids (CUIDs).",
"keywords": [
"cuid",
"identifier"
],
"support": {
"issues": "https://github.com/visus-io/php-cuid2/issues",
"source": "https://github.com/visus-io/php-cuid2/tree/4.1.0"
},
"time": "2024-05-14T13:23:35+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.2",
"ext-pdo": "*",
"ext-sqlite3": "*"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

16
src/lib/History.php Normal file
View File

@ -0,0 +1,16 @@
<?php declare(strict_types=1);
namespace MyPrayerJournal;
/**
* A record of an action taken on a request
*/
class History
{
/**
* @param string $asOf The date/time this entry was made
* @param RequestAction $action The action taken for this history entry
* @param string|null $text The text for this history entry (optional)
*/
public function __construct(public string $asOf, public RequestAction $action, public ?string $text = null) { }
}

15
src/lib/Note.php Normal file
View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace MyPrayerJournal;
/**
* A note entered on a prayer request
*/
class Note
{
/**
* @param string $asOf The date/time this note was recorded
* @param string $text The text of the note
*/
public function __construct(public string $asOf, public string $text) { }
}

15
src/lib/Recurrence.php Normal file
View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace MyPrayerJournal;
/**
* The recurrence for a prayer request
*/
class Recurrence
{
/**
* @param RecurrencePeriod $period The recurrence period
* @param int|null $interval How many of the periods will pass before the request is visible again
*/
public function __construct(public RecurrencePeriod $period, public ?int $interval = null) { }
}

View File

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace MyPrayerJournal;
/**
* The type of recurrence a request can have
*/
enum RecurrencePeriod
{
/** Requests, once prayed, are available again immediately */
case Immediate;
/** Requests, once prayed, appear again in a number of hours */
case Hours;
/** Requests, once prayed, appear again in a number of days */
case Days;
/** Requests, once prayed, appear again in a number of weeks */
case Weeks;
}

33
src/lib/Request.php Normal file
View File

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace MyPrayerJournal;
use Exception;
use Visus\Cuid2\Cuid2;
/**
* A prayer request
*/
class Request
{
/**
* @param string $id The ID for the request
* @param string $enteredOn The date/time this request was originally entered
* @param string $userId The ID of the user to whom this request belongs
* @param string|null $snoozedUntil The date/time the snooze expires for this request (null = not snoozed)
* @param string|null $showAfter The date/time the current recurrence period is over (null = immediate)
* @param Recurrence $recurrence The recurrence for this request
* @param array|History[] $history The history of this request
* @param array|Note[] $notes Notes regarding this request
* @throws Exception If the ID generation fails
*/
public function __construct(public string $id = '', public string $enteredOn = '', public string $userId = '',
public ?string $snoozedUntil = null, public ?string $showAfter = null,
public Recurrence $recurrence = new Recurrence(RecurrencePeriod::Immediate),
public array $history = [], public array $notes = [])
{
if ($id == '') {
$this->id = (new Cuid2())->toString();
}
}
}

21
src/lib/RequestAction.php Normal file
View File

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace MyPrayerJournal;
/**
* An action taken on a prayer request
*/
enum RequestAction
{
/** The request was created */
case Created;
/** The request was marked as having been prayed for */
case Prayed;
/** The request was updated */
case Updated;
/** The request was marked as answered */
case Answered;
}

15
src/public/index.php Normal file
View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
require '../start.php';
html_head('Welcome'); ?>
<article class="container mt-3">
<p>&nbsp;
<p>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>This site is open and available for anyone who wants to use it. To get started, simply click the &ldquo;Log
On&rdquo; link above, and log on with either a Microsoft or Google account. You can also learn more about the
site at the &ldquo;Docs&rdquo; link, also above.
</article><?php
html_foot();

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

104
src/public/script/mpj.js Normal file
View File

@ -0,0 +1,104 @@
"use strict"
/** myPrayerJournal script */
this.mpj = {
/**
* Show a message via toast
* @param {string} message The message to show
*/
showToast (message) {
const [level, msg] = message.split("|||")
let header
if (level !== "success") {
const heading = typ => `<span class="me-auto"><strong>${typ.toUpperCase()}</strong></span>`
header = document.createElement("div")
header.className = "toast-header"
header.innerHTML = heading(level === "warning" ? level : "error")
const close = document.createElement("button")
close.type = "button"
close.className = "btn-close"
close.setAttribute("data-bs-dismiss", "toast")
close.setAttribute("aria-label", "Close")
header.appendChild(close)
}
const body = document.createElement("div")
body.className = "toast-body"
body.innerText = msg
const toastEl = document.createElement("div")
toastEl.className = `toast bg-${level === "error" ? "danger" : level} text-white`
toastEl.setAttribute("role", "alert")
toastEl.setAttribute("aria-live", "assertlive")
toastEl.setAttribute("aria-atomic", "true")
toastEl.addEventListener("hidden.bs.toast", e => e.target.remove())
if (header) toastEl.appendChild(header)
toastEl.appendChild(body)
document.getElementById("toasts").appendChild(toastEl)
new bootstrap.Toast(toastEl, { autohide: level === "success" }).show()
},
/**
* Load local version of Bootstrap CSS if the CDN load failed
*/
ensureCss () {
let loaded = false
for (let i = 0; !loaded && i < document.styleSheets.length; i++) {
loaded = document.styleSheets[i].href.endsWith("bootstrap.min.css")
}
if (!loaded) {
const css = document.createElement("link")
css.rel = "stylesheet"
css.href = "/style/bootstrap.min.css"
document.getElementsByTagName("head")[0].appendChild(css)
}
},
/** Script for the request edit component */
edit: {
/**
* Toggle the recurrence input fields
* @param {Event} e The click event
*/
toggleRecurrence ({ target }) {
const isDisabled = target.value === "Immediate"
;["recurCount", "recurInterval"].forEach(it => document.getElementById(it).disabled = isDisabled)
}
},
/**
* The time zone of the current browser
* @type {string}
**/
timeZone: undefined,
/**
* Derive the time zone from the current browser
*/
deriveTimeZone () {
try {
this.timeZone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone
} catch (_) { }
}
}
htmx.on("htmx:afterOnLoad", function (evt) {
const hdrs = evt.detail.xhr.getAllResponseHeaders()
// Show a message if there was one in the response
if (hdrs.indexOf("x-toast") >= 0) {
mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
}
// Hide a modal window if requested
if (hdrs.indexOf("x-hide-modal") >= 0) {
document.getElementById(evt.detail.xhr.getResponseHeader("x-hide-modal") + "Dismiss").click()
}
})
htmx.on("htmx:configRequest", function (evt) {
// Send the user's current time zone so that we can display local time
if (mpj.timeZone) {
evt.detail.headers["X-Time-Zone"] = mpj.timeZone
}
})
mpj.deriveTimeZone()

View File

@ -0,0 +1,57 @@
nav {
background-color: green;
}
nav .m {
font-weight: 100;
}
nav .p {
font-weight: 400;
}
nav .j {
font-weight: 700;
}
.nav-item a:link,
.nav-item a:visited {
padding: .5rem 1rem;
margin: 0 .5rem;
border-radius: .5rem;
color: white;
text-decoration: none;
}
.nav-item a:hover {
cursor: pointer;
background-color: rgba(255, 255, 255, .2);
}
.nav-item a.is-active-route {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: solid 4px rgba(255, 255, 255, .3);
}
form {
max-width: 60rem;
margin: auto;
}
.action-cell .material-icons {
font-size: 1.1rem ;
}
.material-icons {
vertical-align: bottom;
}
#toastHost {
position: sticky;
bottom: 0;
}
.request-text {
white-space: pre-line
}
footer {
border-top: solid 1px lightgray;
margin: 1rem -1rem 0;
padding: 0 1rem;
}
footer p {
margin: 0;
}

55
src/start.php Normal file
View File

@ -0,0 +1,55 @@
<?php declare(strict_types=1);
const MPJ_VERSION = '4.0.0';
function html_head(string $title): void
{ ?>
<!DOCTYPE html>
<head lang=en>
<meta name=viewport content="width=device-width, initial-scale=1">
<meta name=description content="Online prayer journal - free w/Google or Microsoft account">
<title><?=$title?> &#xab; myPrayerJournal</title>
<link href=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css rel=stylesheet
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin=anonymous>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel=stylesheet>
<link href=/style/style.css rel=stylesheet>
</head><?php
}
function page_link(string $href, string $text, array $attrs = []): void
{ ?>
<a href="<?=$href?>" hx-get="<?=$href?>" hx-target="#top" hx-swap=innerHTML hx-push-url=true<?php
foreach ($attrs as $key => $value) echo " $key=\"" . htmlspecialchars($value) . "\""; ?>><?=$text?></a><?php
}
function html_foot(): void
{
$version = MPJ_VERSION;
while (!str_ends_with($version, '.0')) $version = substr($version, 0, strlen($version) - 2); ?>
<footer class=container-fluid>
<p class="text-muted text-end">
myPrayerJournal <?=$version?><br>
<em>
<small><?php
page_link('/legal/privacy-policy', 'Privacy Policy');
echo ' &bull; ';
page_link('/legal/terms-of-service', 'Terms of Service');
echo ' &bull; '; ?>
<a href=https://git.bitbadger.solutions/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>
Htmx.Script.minified
<script>if (!htmx) document.write('<script src=\"/script/htmx.min.js\"><\/script>')</script>
<script async src=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin=anonymous></script>
<script>
setTimeout(function () {
if (!bootstrap) document.write('<script src=\"/script/bootstrap.bundle.min.js\"><\/script>')
}, 2000)
</script>
<script src=/script/mpj.js></script>
</footer><?php
}