WIP on vanilla PHP framework

This commit is contained in:
danieljsummers committed 2024-06-20 23:06:31 -04:00
1 parent 24c503385e
commit 41853a7645
16 files changed
+562 -6

No files matched your search

+16
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
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
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
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
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) { }
}
+21
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
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
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
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();
+7
View File
@@ -0,0 +1,7 @@
/*!
* Bootstrap v5.3.2 (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slLine truncated
//# sourceMappingURL=bootstrap.bundle.min.js.map
+1
View File
@@ -0,0 +1 @@
(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Y={onLoad:t,process:Pt,on:Z,off:K,trigger:fe,ajax:wr,find:E,findAll:f,closest:v,values:function(e,t){var r=nr(e,t||"post");return r.values},remove:U,addClass:B,removeClass:n,toggleClass:V,takeClass:j,defineExtension:qr,removeExtension:Hr,logAll:X,logNone:F,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false},parseInterval:d,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Y.config.wsBinaryType;return t},version:"1.9.6"};var r={addTriggerHandler:St,bodyContains:oe,canAccessLocalStorage:M,findThisElement:de,filterValues:lr,hasAttribute:o,getAttributeValue:ee,getClosestAttributeValue:re,getClosestMatch:c,getExpressionVars:xr,getHeaders:sr,getInputValues:nr,getInternalData:ie,getSwapSpecification:fr,getTriggerSpecs:Ze,getTarget:ge,makeFragment:l,mergeObjects:se,makeSettleInfo:T,oobSwap:ye,querySelectorExt:le,selectAndSwap:Fe,settleImmediately:Wt,shouldCancel:tt,triggerEvent:fe,triggerErrorEvent:ue,withExtensions:C};var b=["get","post","put","delete","patch"];var w=b.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function Q(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function ee(e,t){return Q(e,t)||Q(e,"data-"+t)}function u(e){return e.parentElement}function te(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function O(e,t,r){var n=ee(t,r);var i=ee(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function re(t,r){var n=null;c(t,function(e){return n=O(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=te().createDocumentFragment()}return i}function H(e){return e.match(/<body/)}function l(e){var t=!H(e);if(Y.config.useTemplateFragments&&t){var r=i("<body><template>"+e+"</template></body>",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("<table>"+e+"</table>",1);case"col":return i("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return i("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return i("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":case"style":return i("<div>"+e+"</div>",1);default:return i(e,0)}}}function ne(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ie(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function ae(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function P(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function oe(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return te().body.contains(e.getRootNode().host)}else{return te().body.contains(e)}}function k(e){return e.trim().split(/\s+/)}function se(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function S(e){try{return JSON.parse(e)}catch(e){y(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t)Line truncated
+104
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()
+57
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
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
}